From e56b175df0d3c0f2a8e1681258c8cc661e7d27de Mon Sep 17 00:00:00 2001 From: Young Gyu Park Date: Wed, 28 Feb 2018 11:27:58 +0900 Subject: Add BaseHandler and handling exception. Test: mma Bug: 72354745 Change-Id: Icf3ff7349b43b667b8738a6d22e96fde4ee811f9 --- gae/app.yaml | 1 + gae/appengine_config.py | 5 + gae/requirements.txt | 5 +- gae/webapp/src/dashboard/build_list.py | 23 +--- gae/webapp/src/dashboard/device_list.py | 24 +--- gae/webapp/src/dashboard/job_list.py | 25 +--- gae/webapp/src/dashboard/schedule_list.py | 25 +--- gae/webapp/src/handlers/__init__.py | 0 gae/webapp/src/handlers/base.py | 213 ++++++++++++++++++++++++++++++ gae/webapp/src/handlers/errors.py | 80 +++++++++++ gae/webapp/src/webapp_config.py | 24 ---- gae/webapp/src/webapp_main.py | 35 ++--- 12 files changed, 334 insertions(+), 126 deletions(-) create mode 100644 gae/appengine_config.py create mode 100644 gae/webapp/src/handlers/__init__.py create mode 100644 gae/webapp/src/handlers/base.py create mode 100644 gae/webapp/src/handlers/errors.py delete mode 100644 gae/webapp/src/webapp_config.py diff --git a/gae/app.yaml b/gae/app.yaml index 9eb0661..8ba46f9 100644 --- a/gae/app.yaml +++ b/gae/app.yaml @@ -6,6 +6,7 @@ threadsafe: true env_variables: ENDPOINTS_SERVICE_NAME: vtslab-schedule-prod.appspot.com ENDPOINTS_SERVICE_VERSION: 2018-02-01r2 + SESSION_SECRET_KEY: '' # [END env_vars] # [START builtins] diff --git a/gae/appengine_config.py b/gae/appengine_config.py new file mode 100644 index 0000000..1003e5e --- /dev/null +++ b/gae/appengine_config.py @@ -0,0 +1,5 @@ +# appengine_config.py +from google.appengine.ext import vendor + +# Add any libraries install in the "lib" folder. +vendor.add('lib') diff --git a/gae/requirements.txt b/gae/requirements.txt index d543738..971ee71 100644 --- a/gae/requirements.txt +++ b/gae/requirements.txt @@ -1,3 +1,2 @@ -google-endpoints==2.4.5 -google-endpoints-api-management==1.3.0 -google-api-python-client \ No newline at end of file +arrow +stripe diff --git a/gae/webapp/src/dashboard/build_list.py b/gae/webapp/src/dashboard/build_list.py index 5d564e0..b3bb4ac 100644 --- a/gae/webapp/src/dashboard/build_list.py +++ b/gae/webapp/src/dashboard/build_list.py @@ -14,11 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import webapp2 -from google.appengine.api import users - -from webapp.src import webapp_config +from webapp.src.handlers.base import BaseHandler from webapp.src.proto import model @@ -90,21 +87,16 @@ def ReadBuildInfo(target_branch=""): return test_builds, device_builds, gsi_builds -class BuildPage(webapp2.RequestHandler): +class BuildPage(BaseHandler): """Main class for /build web page.""" def get(self): """Generates an HTML page based on the build info kept in DB.""" + self.template = "build.html" + target_branch = self.request.get("branch", default_value="") test_builds, device_builds, gsi_builds = ReadBuildInfo(target_branch) - user = users.get_current_user() - if user: - url = users.create_logout_url(self.request.uri) - url_linktext = "Logout" - else: - url = users.create_login_url(self.request.uri) - url_linktext = "Login" manifest_branch_keys = list(set().union( test_builds.keys(), device_builds.keys(), @@ -129,12 +121,7 @@ class BuildPage(webapp2.RequestHandler): all_builds[manifest_branch_key]["gsi"] = [] template_values = { - "user": user, "all_builds": all_builds, - "url": url, - "url_linktext": url_linktext, } - template = webapp_config.JINJA_ENVIRONMENT.get_template( - "static/build.html") - self.response.write(template.render(template_values)) + self.render(template_values) diff --git a/gae/webapp/src/dashboard/device_list.py b/gae/webapp/src/dashboard/device_list.py index 3869469..384b1ee 100644 --- a/gae/webapp/src/dashboard/device_list.py +++ b/gae/webapp/src/dashboard/device_list.py @@ -16,47 +16,33 @@ # import datetime -import webapp2 -from google.appengine.api import users - -from webapp.src import webapp_config +from webapp.src.handlers.base import BaseHandler from webapp.src.proto import model -class DevicePage(webapp2.RequestHandler): +class DevicePage(BaseHandler): """Main class for /device web page.""" def get(self): """Generates an HTML page based on the device info kept in DB.""" + self.template = "device.html" + device_query = model.DeviceModel.query() devices = device_query.fetch() lab_query = model.LabModel.query() labs = lab_query.fetch() - user = users.get_current_user() - if user: - url = users.create_logout_url(self.request.uri) - url_linktext = "Logout" - else: - url = users.create_login_url(self.request.uri) - url_linktext = "Login" - if devices: devices = sorted( devices, key=lambda x: (x.hostname, x.product, x.status), reverse=False) template_values = { - "user": user, "now": datetime.datetime.now(), "devices": devices, "labs": labs, - "url": url, - "url_linktext": url_linktext, } - template = webapp_config.JINJA_ENVIRONMENT.get_template( - "static/device.html") - self.response.write(template.render(template_values)) + self.render(template_values) diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index 5a059a8..181e0a1 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -15,37 +15,22 @@ # limitations under the License. # -import webapp2 - -from google.appengine.api import users - -from webapp.src import webapp_config +from webapp.src.handlers.base import BaseHandler from webapp.src.proto import model -class JobPage(webapp2.RequestHandler): +class JobPage(BaseHandler): """Main class for /job web page.""" def get(self): """Generates an HTML page based on the job queue info kept in DB.""" + self.template = "job.html" + job_query = model.JobModel.query() jobs = job_query.fetch() - user = users.get_current_user() - if user: - url = users.create_logout_url(self.request.uri) - url_linktext = "Logout" - else: - url = users.create_login_url(self.request.uri) - url_linktext = "Login" - template_values = { - "user": user, "jobs": sorted(jobs, key=lambda x: x.timestamp, reverse=True), - "url": url, - "url_linktext": url_linktext, } - template = webapp_config.JINJA_ENVIRONMENT.get_template( - "static/job.html") - self.response.write(template.render(template_values)) + self.render(template_values) diff --git a/gae/webapp/src/dashboard/schedule_list.py b/gae/webapp/src/dashboard/schedule_list.py index c3c7c56..0f00b79 100644 --- a/gae/webapp/src/dashboard/schedule_list.py +++ b/gae/webapp/src/dashboard/schedule_list.py @@ -15,42 +15,27 @@ # limitations under the License. # -import webapp2 - -from google.appengine.api import users - -from webapp.src import webapp_config +from webapp.src.handlers.base import BaseHandler from webapp.src.proto import model -class SchedulePage(webapp2.RequestHandler): +class SchedulePage(BaseHandler): """Main class for /schedule web page.""" def get(self): """Generates an HTML page based on the task schedules kept in DB.""" + self.template = "schedule.html" + schedule_query = model.ScheduleModel.query() schedules = schedule_query.fetch() - user = users.get_current_user() - if user: - url = users.create_logout_url(self.request.uri) - url_linktext = "Logout" - else: - url = users.create_login_url(self.request.uri) - url_linktext = "Login" - if schedules: schedules = sorted( schedules, key=lambda x: (x.manifest_branch, x.build_target), reverse=False) template_values = { - "user": user, "schedules": schedules, - "url": url, - "url_linktext": url_linktext, } - template = webapp_config.JINJA_ENVIRONMENT.get_template( - "static/schedule.html") - self.response.write(template.render(template_values)) \ No newline at end of file + self.render(template_values) \ No newline at end of file diff --git a/gae/webapp/src/handlers/__init__.py b/gae/webapp/src/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gae/webapp/src/handlers/base.py b/gae/webapp/src/handlers/base.py new file mode 100644 index 0000000..28d5393 --- /dev/null +++ b/gae/webapp/src/handlers/base.py @@ -0,0 +1,213 @@ +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import httplib +import logging +import os +import urlparse + +import arrow +import stripe +import webapp2 +from google.appengine.api import users +from webapp2_extras import jinja2 as wa2_jinja2 +from webapp2_extras import sessions + +import errors + + +class BaseHandler(webapp2.RequestHandler): + """BaseHandler for all requests.""" + + def initialize(self, request, response): + """Initializes this request handler.""" + webapp2.RequestHandler.initialize(self, request, response) + self.session_backend = 'datastore' + + def verify_origin(self): + """This function will check the request is comming from the same domain.""" + server_host = os.environ.get('ENDPOINTS_SERVICE_NAME') + request_host = self.request.headers.get('Host') + request_referer = self.request.headers.get('Referer') + if request_referer: + request_referer = urlparse.urlsplit(request_referer)[1] + else: + request_referer = request_host + logging.info('server: %s, request: %s', server_host, request_referer) + if server_host and request_referer and server_host != request_referer: + raise errors.Error(httplib.FORBIDDEN) + + def dispatch(self): + """Dispatch the request. + + This will first check if there's a handler_method defined + in the matched route, and if not it'll use the method correspondent to + the request method (get(), post() etc). + """ + self.session_store = sessions.get_store(request=self.request) + # Forwards the method for RESTful support. + self.forward_method() + # Security headers. + # https://www.owasp.org/index.php/List_of_useful_HTTP_headers + self.response.headers['x-content-type-options'] = 'nosniff' + self.response.headers['x-frame-options'] = 'SAMEORIGIN' + self.response.headers['x-xss-protection'] = '1; mode=block' + try: + webapp2.RequestHandler.dispatch(self) + finally: + self.session_store.save_sessions(self.response) + # Disabled for now because host is appspot.com in production. + #self.verify_origin() + + @webapp2.cached_property + def session(self): + # Returns a session using the default cookie key. + return self.session_store.get_session() + + def handle_exception(self, exception, debug=False): + """Render the exception as HTML.""" + logging.exception(exception) + + # Create response dictionary and status defaults. + tpl = 'error.html' + status = httplib.INTERNAL_SERVER_ERROR + resp_dict = { + 'message': 'A server error occurred.', + } + url_parts = self.urlsplit() + redirect_url = '%s?%s' % (url_parts[2], url_parts[4]) + + # Use error code if a HTTPException, or generic 500. + if isinstance(exception, webapp2.HTTPException): + status = exception.code + resp_dict['message'] = exception.detail + elif isinstance(exception, errors.FormValidationError): + status = exception.code + resp_dict['message'] = exception.msg + resp_dict['errors'] = exception.errors + self.session['form_errors'] = exception.errors + # Redirect user to current view URL. + return self.redirect(redirect_url) + elif isinstance(exception, stripe.StripeError): + status = exception.http_status + resp_dict['errors'] = exception.json_body['error']['message'] + self.session['form_errors'] = [ + exception.json_body['error']['message'] + ] + return self.redirect(redirect_url) + elif isinstance(exception, (errors.Error, errors.AclError)): + status = exception.code + resp_dict['message'] = exception.msg + + resp_dict['status'] = status + + # Render output. + self.response.status_int = status + self.response.status_message = httplib.responses[status] + # Render the exception response into the error template. + self.response.write(self.jinja2.render_template(tpl, **resp_dict)) + + # @Override + def get(self, *args, **kwargs): + self.abort(httplib.NOT_IMPLEMENTED) + + # @Override + def post(self, *args, **kwargs): + self.abort(httplib.NOT_IMPLEMENTED) + + # @Override + def put(self, *args, **kwargs): + self.abort(httplib.NOT_IMPLEMENTED) + + # @Override + def delete(self, *args, **kwargs): + self.abort(httplib.NOT_IMPLEMENTED) + + # @Override + def head(self, *args, **kwargs): + pass + + def urlsplit(self): + """Return a tuple of the URL.""" + return urlparse.urlsplit(self.request.url) + + def path(self): + """Returns the path of the current URL.""" + return self.urlsplit()[2] + + def forward_method(self): + """Check for a method override param and change in the request.""" + valid = (None, 'get', 'post', 'put', 'delete', 'head', 'options') + method = self.request.POST.get('__method__') + if not method: # Backbone's _method parameter. + method = self.request.POST.get('_method') + if method not in valid: + logging.debug('Invalid method %s requested!', method) + method = None + logging.debug('Method being changed from %s to %s by request', + self.request.route.handler_method, method) + self.request.route.handler_method = method + + def render(self, resp, status=httplib.OK): + """Render the response as HTML.""" + user = users.get_current_user() + if user: + url = users.create_logout_url(self.request.uri) + url_linktext = "Logout" + else: + url = users.create_login_url(self.request.uri) + url_linktext = "Login" + + resp.update({ + # Defaults go here. + 'now': arrow.utcnow(), + 'dest_url': str(self.request.get('dest_url', '')), + 'form_errors': self.session.pop('form_errors', []), + 'user': user, + 'url': url, + 'url_linktext': url_linktext, + }) + + if 'preload' not in resp: + resp['preload'] = {} + + self.response.status_int = status + self.response.status_message = httplib.responses[status] + self.response.write(self.jinja2.render_template(self.template, **resp)) + + @webapp2.cached_property + def jinja2(self): + """Returns a Jinja2 renderer cached in the app registry.""" + jinja_config = { + 'template_path': + os.path.join(os.path.dirname(__file__), "../../static"), + 'compiled_path': + None, + 'force_compiled': + False, + 'environment_args': { + 'autoescape': True, + 'extensions': [ + 'jinja2.ext.autoescape', + 'jinja2.ext.with_', + ], + }, + 'globals': + None, + 'filters': + None, + } + return wa2_jinja2.Jinja2(app=self.app, config=jinja_config) diff --git a/gae/webapp/src/handlers/errors.py b/gae/webapp/src/handlers/errors.py new file mode 100644 index 0000000..fde95c0 --- /dev/null +++ b/gae/webapp/src/handlers/errors.py @@ -0,0 +1,80 @@ +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import httplib + + +class Error(Exception): + """Base Exception for handlers. + + Attributes: + code: HTTP 1.1 status code + msg: the W3C names for HTTP 1.1 status code + """ + + def __init__(self, code=None, msg=None): + self.code = code or httplib.INTERNAL_SERVER_ERROR + self.msg = msg or httplib.responses[self.code] + super(Error, self).__init__(self.code, self.msg) + + def __str__(self): + return repr([self.code, self.msg]) + + +class FormValidationError(Error): + """Form Validtion Exception for handlers.""" + + def __init__(self, code=None, msg=None, errors=None): + self.code = code or httplib.BAD_REQUEST + self.msg = msg or httplib.responses[self.code] + self.errors = errors or [] + super(FormValidationError, self).__init__(self.code, self.msg) + + def __str__(self): + return repr([self.code, self.msg, self.errors]) + + +class AclError(Error): + """Base ACL error.""" + + def __init__(self, code): + self.code = code + self.msg = httplib.responses[code] + super(AclError, self).__init__(self.code, self.msg) + + def __str__(self): + return repr([self.code, self.msg]) + + +class NotFoundError(AclError): + """The item being access does not exist.""" + + def __init__(self): + super(NotFoundError, self).__init__(httplib.NOT_FOUND) + + +class UnauthorizedError(AclError): + """The current user is not authenticated.""" + + def __init__(self): + super(UnauthorizedError, self).__init__(httplib.UNAUTHORIZED) + + +class ForbiddenError(AclError): + """The current user does not have proper permission.""" + + def __init__(self): + super(ForbiddenError, self).__init__(httplib.FORBIDDEN) diff --git a/gae/webapp/src/webapp_config.py b/gae/webapp/src/webapp_config.py deleted file mode 100644 index fc70883..0000000 --- a/gae/webapp/src/webapp_config.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2017 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import jinja2 -import os - - -JINJA_ENVIRONMENT = jinja2.Environment( - loader=jinja2.FileSystemLoader(os.path.join(os.path.dirname(__file__), - "..")), - extensions=['jinja2.ext.autoescape'], - autoescape=True) diff --git a/gae/webapp/src/webapp_main.py b/gae/webapp/src/webapp_main.py index 2b787d8..3c52add 100644 --- a/gae/webapp/src/webapp_main.py +++ b/gae/webapp/src/webapp_main.py @@ -15,45 +15,36 @@ # limitations under the License. # -import webapp2 - -from google.appengine.api import users +import os -from webapp.src import webapp_config +import webapp2 from webapp.src.dashboard import build_list from webapp.src.dashboard import device_list from webapp.src.dashboard import job_list from webapp.src.dashboard import schedule_list -from webapp.src.scheduler import periodic +from webapp.src.handlers.base import BaseHandler from webapp.src.scheduler import device_heartbeat from webapp.src.scheduler import job_heartbeat +from webapp.src.scheduler import periodic from webapp.src.tasks import indexing -class MainPage(webapp2.RequestHandler): +class MainPage(BaseHandler): """Main web page request handler.""" def get(self): """Generates an HTML page.""" - user = users.get_current_user() - if user: - url = users.create_logout_url(self.request.uri) - url_linktext = "Logout" - else: - url = users.create_login_url(self.request.uri) - url_linktext = "Login" + self.template = "index.html" - template_values = { - "user": user, - "url": url, - "url_linktext": url_linktext, - } + template_values = {} - template = webapp_config.JINJA_ENVIRONMENT.get_template( - "static/index.html") - self.response.write(template.render(template_values)) + self.render(template_values) +config = {} +config['webapp2_extras.sessions'] = { + 'secret_key': os.environ.get('SESSION_SECRET_KEY'), +} app = webapp2.WSGIApplication([ ("/", MainPage), @@ -66,4 +57,4 @@ app = webapp2.WSGIApplication([ ("/tasks/device_heartbeat", device_heartbeat.PeriodicDeviceHeartBeat), ("/tasks/job_heartbeat", job_heartbeat.PeriodicJobHeartBeat), ("/tasks/indexing", indexing.CreateIndex) -], debug=False) +], config=config, debug=False) -- cgit v1.2.3 From 061352991ab78da906db2d93d20d03d84b6361f0 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 6 Mar 2018 13:38:26 +0900 Subject: Updated requirements.txt Test: gcloud app deploy Bug: 72354745 --- gae/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/gae/requirements.txt b/gae/requirements.txt index 971ee71..daa5510 100644 --- a/gae/requirements.txt +++ b/gae/requirements.txt @@ -1,2 +1,3 @@ +google-api-python-client arrow stripe -- cgit v1.2.3 From 88f636896affa340620f30f183bf5b7b1f5b6fd8 Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Thu, 22 Mar 2018 16:13:40 -0700 Subject: product type checking is case in-sensitive Test: deployed to GAE Change-Id: Ide877394ffef9cbf908534978e207e3a4c4d05b2 --- gae/webapp/src/scheduler/periodic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gae/webapp/src/scheduler/periodic.py b/gae/webapp/src/scheduler/periodic.py index 83d3632..4852baa 100644 --- a/gae/webapp/src/scheduler/periodic.py +++ b/gae/webapp/src/scheduler/periodic.py @@ -271,7 +271,7 @@ class PeriodicScheduler(webapp2.RequestHandler): Status.DEVICE_STATUS_DICT["ready"] ]) and (device.scheduling_status == Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) - and device.product == target_product_type): + and device.product.lower() == target_product_type.lower()): self.logger.Println( "- a device found %s" % device.serial) if device.hostname not in available_devices: -- cgit v1.2.3 From 1be3f4d0059bfd12c9bef1b394100c2569f674d8 Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Mon, 2 Apr 2018 16:50:24 -0700 Subject: add simple device stats Test: deployed to GAE Bug: 75325573 Change-Id: Ie677945a112a2485fd7ef1fdaef715c1c3a1d304 --- gae/webapp/src/dashboard/device_list.py | 33 +++++++++++++++++++++++++++++++++ gae/webapp/static/device.html | 3 ++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/gae/webapp/src/dashboard/device_list.py b/gae/webapp/src/dashboard/device_list.py index 384b1ee..fd0e53c 100644 --- a/gae/webapp/src/dashboard/device_list.py +++ b/gae/webapp/src/dashboard/device_list.py @@ -19,6 +19,21 @@ import datetime from webapp.src.handlers.base import BaseHandler from webapp.src.proto import model +from webapp.src import vtslab_status + + +class DeviceStats(object): + """Device stats class. + + Attributes: + total: int, total device count. + utilization: int, the device utilization ratio (%). + error_ratio: int, the device error ratio (%). + """ + + total = 0 + utilization = -1 + error_ratio = -1 class DevicePage(BaseHandler): @@ -34,15 +49,33 @@ class DevicePage(BaseHandler): lab_query = model.LabModel.query() labs = lab_query.fetch() + stats = DeviceStats() if devices: devices = sorted( devices, key=lambda x: (x.hostname, x.product, x.status), reverse=False) + count_active, count_idle, count_error = 0, 0, 0 + for device in devices: + if device.scheduling_status == vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["free"]: + if (device.status == vtslab_status.DEVICE_STATUS_DICT["error"] + or device.status == vtslab_status.DEVICE_STATUS_DICT["no-response"]): + count_error += 1 + else: + # it shouldn't be in use state. + count_idle += 1 + else: + # includes both use and reserved + count_active += 1 + + stats.total = count_active + count_idle + count_error + stats.utilization = count_active * 100 / stats.total + stats.error_ratio = count_error * 100 / stats.total template_values = { "now": datetime.datetime.now(), "devices": devices, "labs": labs, + "stats": stats } self.render(template_values) diff --git a/gae/webapp/static/device.html b/gae/webapp/static/device.html index 87c309b..23ef8ce 100644 --- a/gae/webapp/static/device.html +++ b/gae/webapp/static/device.html @@ -57,7 +57,8 @@

Device List

- Now: {{ now }} + Utilization: {{ stats.utilization }}%, Error Ratio: {{ stats.error_ratio }}%, Total Device Count: {{ stats.total}} + (Now: {{ now }})
# -- cgit v1.2.3 From ea9d4708a798321bc904590baa0e1d9dde10346e Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Mon, 2 Apr 2018 15:35:59 -0700 Subject: simplify scheduler logging Test: deployed to GAE Bug: 75325573 Change-Id: I1cdc039b1056cb6fdc5f515f2c8f43c30c494e80 --- gae/webapp/src/scheduler/periodic.py | 152 ++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 72 deletions(-) diff --git a/gae/webapp/src/scheduler/periodic.py b/gae/webapp/src/scheduler/periodic.py index a957c0e..9d48748 100644 --- a/gae/webapp/src/scheduler/periodic.py +++ b/gae/webapp/src/scheduler/periodic.py @@ -104,70 +104,78 @@ class PeriodicScheduler(webapp2.RequestHandler): if schedules: for schedule in schedules: - self.logger.Println("Schedule: %s (%s %s)" % + self.logger.Println("") + self.logger.Println("Schedule: %s (branch: %s)" % (schedule.test_name, - schedule.manifest_branch, - schedule.build_target)) + schedule.manifest_branch)) + self.logger.Println("Device: %s" % schedule.build_target) self.logger.Indent() - if self.NewPeriod(schedule): - self.logger.Println("- Need new job") - target_host, target_device, target_device_serials =\ - self.SelectTargetLab(schedule) - self.logger.Println("- Target host: %s" % target_host) - self.logger.Println("- Target device: %s" % target_device) - self.logger.Println( - "- Target serials: %s" % target_device_serials) - # TODO: update device status - - # create job and add. - if target_host: - new_job = model.JobModel() - new_job.hostname = target_host - new_job.priority = schedule.priority - new_job.test_name = schedule.test_name - new_job.require_signed_device_build = ( - schedule.require_signed_device_build) - new_job.device = target_device - new_job.period = schedule.period - new_job.serial.extend(target_device_serials) - new_job.build_storage_type = schedule.build_storage_type - new_job.manifest_branch = schedule.manifest_branch - new_job.build_target = schedule.build_target - new_job.shards = schedule.shards - new_job.param = schedule.param - new_job.retry_count = schedule.retry_count - new_job.gsi_storage_type = schedule.gsi_storage_type - new_job.gsi_branch = schedule.gsi_branch - new_job.gsi_build_target = schedule.gsi_build_target - new_job.gsi_pab_account_id = schedule.gsi_pab_account_id - new_job.test_storage_type = schedule.test_storage_type - new_job.test_branch = schedule.test_branch - new_job.test_build_target = schedule.test_build_target - new_job.test_pab_account_id = ( - schedule.test_pab_account_id) - - new_job.build_id = "" - - if new_job.build_storage_type == ( - Status.STORAGE_TYPE_DICT["PAB"]): - new_job.build_id = self.FindBuildId(new_job) - if new_job.build_id: - self.ReserveDevices(target_device_serials) - new_job.status = Status.JOB_STATUS_DICT[ - "ready"] - new_job.timestamp = datetime.datetime.now() - new_job.put() - self.logger.Println("NEW JOB") - else: - self.logger.Println("NO BUILD FOUND") - elif new_job.build_storage_type == ( - Status.STORAGE_TYPE_DICT["GCS"]): - new_job.status = Status.JOB_STATUS_DICT["ready"] - new_job.timestamp = datetime.datetime.now() - new_job.put() - self.logger.Println("NEW JOB - GCS") - else: - self.logger.Println("Unexpected storage type.") + if not self.NewPeriod(schedule): + self.logger.Println("- Skipped") + self.logger.Unindent() + continue + + target_host, target_device, target_device_serials = ( + self.SelectTargetLab(schedule)) + if not target_host: + self.logger.Unindent() + continue + + self.logger.Println("- Target host: %s" % target_host) + self.logger.Println("- Target device: %s" % target_device) + self.logger.Println( + "- Target serials: %s" % target_device_serials) + # TODO: update device status + + # create job and add. + new_job = model.JobModel() + new_job.hostname = target_host + new_job.priority = schedule.priority + new_job.test_name = schedule.test_name + new_job.require_signed_device_build = ( + schedule.require_signed_device_build) + new_job.device = target_device + new_job.period = schedule.period + new_job.serial.extend(target_device_serials) + new_job.build_storage_type = schedule.build_storage_type + new_job.manifest_branch = schedule.manifest_branch + new_job.build_target = schedule.build_target + new_job.shards = schedule.shards + new_job.param = schedule.param + new_job.retry_count = schedule.retry_count + new_job.gsi_storage_type = schedule.gsi_storage_type + new_job.gsi_branch = schedule.gsi_branch + new_job.gsi_build_target = schedule.gsi_build_target + new_job.gsi_pab_account_id = schedule.gsi_pab_account_id + new_job.test_storage_type = schedule.test_storage_type + new_job.test_branch = schedule.test_branch + new_job.test_build_target = schedule.test_build_target + new_job.test_pab_account_id = ( + schedule.test_pab_account_id) + + new_job.build_id = "" + + if new_job.build_storage_type == ( + Status.STORAGE_TYPE_DICT["PAB"]): + new_job.build_id = self.FindBuildId(new_job) + if new_job.build_id: + self.ReserveDevices(target_device_serials) + new_job.status = Status.JOB_STATUS_DICT[ + "ready"] + new_job.timestamp = datetime.datetime.now() + new_job.put() + self.logger.Println("NEW JOB") + else: + self.logger.Println("NO BUILD FOUND") + elif new_job.build_storage_type == ( + Status.STORAGE_TYPE_DICT["GCS"]): + new_job.status = Status.JOB_STATUS_DICT["ready"] + new_job.timestamp = datetime.datetime.now() + new_job.put() + self.logger.Println("NEW JOB - GCS") + else: + self.logger.Println("Unexpected storage type (%s)." % + new_job.build_storage_type) self.logger.Unindent() @@ -259,8 +267,8 @@ class PeriodicScheduler(webapp2.RequestHandler): continue target_lab, target_product_type = target_device.split("/") - self.logger.Println("- Seeking product %s in lab %s" % - (target_product_type, target_lab)) + self.logger.Println("- Lab %s (device: %s)" % + (target_lab, target_product_type)) self.logger.Indent() lab_query = model.LabModel.query(model.LabModel.name == target_lab) target_labs = lab_query.fetch() @@ -268,8 +276,7 @@ class PeriodicScheduler(webapp2.RequestHandler): available_devices = {} if target_labs: for lab in target_labs: - self.logger.Println("- target lab found") - self.logger.Println("- target device %s %s" % + self.logger.Println("- Host: %s (device: %s)" % (lab.hostname, target_product_type)) self.logger.Indent() device_query = model.DeviceModel.query( @@ -277,8 +284,6 @@ class PeriodicScheduler(webapp2.RequestHandler): host_devices = device_query.fetch() for device in host_devices: - self.logger.Println("- check device %s %s" % - (device.status, device.product)) if ((device.status in [ Status.DEVICE_STATUS_DICT["fastboot"], Status.DEVICE_STATUS_DICT["online"], @@ -287,19 +292,22 @@ class PeriodicScheduler(webapp2.RequestHandler): Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) and device.product.lower() == target_product_type.lower()): self.logger.Println( - "- a device found %s" % device.serial) + "- Found %s %s %s" % (device.product, + device.status, + device.serial)) if device.hostname not in available_devices: available_devices[device.hostname] = set() available_devices[device.hostname].add( device.serial) self.logger.Unindent() for host in available_devices: - self.logger.Println("- len(devices) %s >= shards %s ?" % - (len(available_devices[host]), - schedule.shards)) if len(available_devices[host]) >= schedule.shards: + self.logger.Println("All devices found.") self.logger.Unindent() return host, target_device, list( available_devices[host])[:schedule.shards] + self.logger.Println( + "- Not enough devices found, while %s required." % ( + schedule.shards)) self.logger.Unindent() return None, None, [] -- cgit v1.2.3 From bfeb8edfa6c5dc0ded45d4786b9084312e58d533 Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Mon, 2 Apr 2018 17:24:15 -0700 Subject: simple job stats page Test: deployed to GAE Bug: 75325573 Change-Id: I2def163c0ef6173c23ca9ee0946c0ddfd06fc4dc --- gae/webapp/src/dashboard/job_list.py | 54 ++++++++++++++++++++++++++++++++++++ gae/webapp/static/job.html | 47 ++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index 3f5fe74..cfb57a1 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -22,6 +22,28 @@ from webapp.src.handlers.base import BaseHandler from webapp.src.proto import model +class JobStats(object): + """Job stats class. + + Attributes: + created: int, the number of created jobs. + completed: int, the number of completed jobs. + failed: int, the number of failed jobs. + expired: int, the number of expired jobs. + running: int, the number of running jobs. + ready: int, the number of ready jobs. + unknown: int, the number of unknown jobs. + """ + + created = 0 + completed = 0 + failed = 0 + expired = 0 + running = 0 + ready = 0 + unknown = 0 + + class JobPage(BaseHandler): """Main class for /job web page.""" @@ -32,13 +54,45 @@ class JobPage(BaseHandler): job_query = model.JobModel.query() jobs = job_query.fetch() + now = datetime.datetime.now() + stats_all = JobStats() + stats_24hrs = JobStats() + if jobs: + for job in jobs: + self._UpdateStats(stats_all, job) + if now - job.timestamp <= datetime.timedelta(hours=24): + self._UpdateStats(stats_24hrs, job) + template_values = { "jobs": sorted(jobs, key=lambda x: x.timestamp, reverse=True), + "stats_all": stats_all, + "stats_24hrs": stats_24hrs } self.render(template_values) + def _UpdateStats(self, stats, job): + """Updates the stats using the state info of a given job. + + Args: + stats: JobStats, the stats class to update. + job: JobModel, the job to check. + """ + stats.created += 1 + if job.status == vtslab_status.JOB_STATUS_DICT["complete"]: + stats.completed += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["leased"]: + stats.running += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["ready"]: + stats.ready += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["infra-err"]: + stats.failed += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["expired"]: + stats.expired += 1 + else: + stats.unknown += 1 + class CreateJobTemplatePage(BaseHandler): """Main class for /create_job_template web page.""" diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index b972f2e..b1ac3bb 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -56,19 +56,46 @@

Job Queue

-

Shortcuts: 8.0 (oc) 8.1 (oc-mr1) 9.0 (pi)

-

Create a job

-

{{ message }}

- +
- -
+

Shortcuts: 8.0 (oc) 8.1 (oc-mr1) 9.0 (pi)

+

Create a job

+

{{ message }}

+ + + +
+ +
+
+ + + + +
Stats + Created + Completed + Running/Ready + Failed/Expired +
24 Hours + {{ stats_24hrs.created }} + {{ stats_24hrs.completed * 100 / stats_24hrs.created }}% + {{ (stats_24hrs.running + stats_24hrs.ready) * 100 / stats_24hrs.created }}% + {{ (stats_24hrs.failed + stats_24hrs.expired) * 100 / stats_24hrs.created }}% +
All + {{ stats_all.created }} + {{ stats_all.completed * 100 / stats_all.created }}% + {{ (stats_all.running + stats_all.ready) * 100 / stats_all.created }}% + {{ (stats_all.failed + stats_all.expired) * 100 / stats_all.created }}% +
+
+ (Now: {{ now }})
# -- cgit v1.2.3 From 2d0e85971e48a6fafe2ccc1613e4f22893f6c5ce Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 5 Apr 2018 19:29:17 +0900 Subject: Added unit test codes and runner. Test: vti/test_serving/gae$ ./script/run-unittest.sh Bug: 77617865 --- gae/app.yaml | 2 + gae/script/run-unittest.sh | 17 ++++ gae/testrunner.py | 128 ++++++++++++++++++++++++++++++ gae/webapp/src/scheduler/periodic_test.py | 108 +++++++++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100755 gae/script/run-unittest.sh create mode 100644 gae/testrunner.py create mode 100644 gae/webapp/src/scheduler/periodic_test.py diff --git a/gae/app.yaml b/gae/app.yaml index 8ba46f9..e9fce8f 100644 --- a/gae/app.yaml +++ b/gae/app.yaml @@ -50,4 +50,6 @@ skip_files: - ^(.*/)?.*/RCS/.*$ - ^(.*/)?\..*$ - ^script/*$ +- testrunner.py +- .*_test.py$ # [END exclude] diff --git a/gae/script/run-unittest.sh b/gae/script/run-unittest.sh new file mode 100755 index 0000000..4e20b0b --- /dev/null +++ b/gae/script/run-unittest.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Copyright 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +python testrunner.py diff --git a/gae/testrunner.py b/gae/testrunner.py new file mode 100644 index 0000000..8294f44 --- /dev/null +++ b/gae/testrunner.py @@ -0,0 +1,128 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""App Engine local test runner. + +This program handles properly importing the App Engine SDK so that test modules +can use google.appengine.* APIs and the Google App Engine testbed. + +Example invocation: + + $ python testrunner.py [--sdk-path ~/google-cloud-sdk] +""" + +import argparse +import os +import subprocess +import sys +import unittest + + +def ExecuteOneShellCommand(cmd): + """Executes one shell command and returns (stdout, stderr, exit_code). + + Args: + cmd: string, a shell command. + + Returns: + tuple(string, string, int), containing stdout, stderr, exit_code of + the shell command. + """ + p = subprocess.Popen( + str(cmd), shell=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + return (stdout, stderr, p.returncode) + + +def fixup_paths(path): + """Adds GAE SDK path to system path and appends it to the google path + if that already exists.""" + # Not all Google packages are inside namespace packages, which means + # there might be another non-namespace package named `google` already on + # the path and simply appending the App Engine SDK to the path will not + # work since the other package will get discovered and used first. + # This emulates namespace packages by first searching if a `google` package + # exists by importing it, and if so appending to its module search path. + try: + import google + google.__path__.append("{0}/google".format(path)) + except ImportError: + pass + + sys.path.insert(0, path) + + +def main(sdk_path, test_path, test_pattern): + + if not sdk_path: + # Get sdk path by running gcloud command. + stdout, stderr, _ = ExecuteOneShellCommand( + "gcloud info --format='value(installation.sdk_root)'") + + if stderr: + print("Cannot find google cloud sdk path.") + return 1 + sdk_path = str.strip(stdout) + + # If the SDK path points to a Google Cloud SDK installation + # then we should alter it to point to the GAE platform location. + if os.path.exists(os.path.join(sdk_path, 'platform/google_appengine')): + sdk_path = os.path.join(sdk_path, 'platform/google_appengine') + + # Make sure google.appengine.* modules are importable. + fixup_paths(sdk_path) + + # Make sure all bundled third-party packages are available. + import dev_appserver + dev_appserver.fix_sys_path() + + # Loading appengine_config from the current project ensures that any + # changes to configuration there are available to all tests (e.g. + # sys.path modifications, namespaces, etc.) + try: + import appengine_config + (appengine_config) + except ImportError: + print('Note: unable to import appengine_config.') + + # Discover and run tests. + suite = unittest.loader.TestLoader().discover(test_path, test_pattern) + return unittest.TextTestRunner(verbosity=2).run(suite) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + '--sdk_path', + help='The path to the Google App Engine SDK or the Google Cloud SDK.', + default=None) + parser.add_argument( + '--test-path', + help='The path to look for tests, defaults to the current directory.', + default=os.getcwd()) + parser.add_argument( + '--test-pattern', + help='The file pattern for test modules, defaults to *_test.py.', + default='*_test.py') + + args = parser.parse_args() + + result = main(args.sdk_path, args.test_path, args.test_pattern) + + if not result.wasSuccessful(): + sys.exit(1) diff --git a/gae/webapp/src/scheduler/periodic_test.py b/gae/webapp/src/scheduler/periodic_test.py new file mode 100644 index 0000000..5839d6e --- /dev/null +++ b/gae/webapp/src/scheduler/periodic_test.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.proto import model +from webapp.src.scheduler import periodic + +from google.appengine.ext import ndb +from google.appengine.ext import testbed + + +class PeriodicSchedulerTest(unittest.TestCase): + """Tests for PeriodicScheduler. + + Attributes: + testbed: A Testbed instance which provides local unit testing. + scheduler: A mock periodic.PeriodicScheduler. + """ + + def setUp(self): + """Initializes test""" + # Create the Testbed class instance and initialize service stubs. + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.init_datastore_v3_stub() + self.testbed.init_memcache_stub() + # Clear cache between tests. + ndb.get_context().clear_cache() + # Mocking PeriodicScheduler and essential methods. + self.scheduler = periodic.PeriodicScheduler(mock.Mock()) + self.scheduler.response = mock.Mock() + self.scheduler.response.write = mock.Mock() + + def tearDown(self): + self.testbed.deactivate() + + def testSimpleJobCreation(self): + """Asserts a job is created. + + This test defines that each model only has a single entity, and asserts + that a job is created. + """ + schedule = model.ScheduleModel() + schedule.schedule_type = "test" + schedule.test_name = "vts/vts" + schedule.manifest_branch = "branch1" + schedule.build_target = "product1-type1" + schedule.device = ["test_lab1/product1"] + schedule.shards = 1 + schedule.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + schedule.require_signed_device_build = False + schedule.put() + + lab = model.LabModel() + lab.name = "test_lab1" + lab.hostname = "test_lab1_host1" + lab.put() + + device = model.DeviceModel() + device.status = Status.DEVICE_STATUS_DICT["fastboot"] + device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT["free"] + device.product = "product1" + device.serial = "serial1" + device.hostname = "test_lab1_host1" + device.put() + + build = model.BuildModel() + build.manifest_branch = "branch1" + build.build_id = "0000000" + build.build_target = "product1" + build.build_type = "type1" + build.put() + + self.scheduler.get() + self.assertEqual(1, len(model.JobModel.query().fetch())) + print("A job is created successfully.") + + device_query = model.DeviceModel.query( + model.DeviceModel.serial == "serial1") + device = device_query.fetch()[0] + self.assertEqual(Status.DEVICE_SCHEDULING_STATUS_DICT["reserved"], + device.scheduling_status) + print("A device is reserved successfully.") + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From a0e13220473505a0d1ee4cd786611abe81a2b5ec Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 2 Apr 2018 16:55:13 +0900 Subject: Reduced the numbers of indexed fields. Test: tested on staging server for 3 days. Bug: 77618305 --- gae/index.yaml | 53 ++++----------------- gae/webapp/src/proto/model.py | 108 +++++++++++++++++++++--------------------- 2 files changed, 64 insertions(+), 97 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index 6b9e317..e8c2d0d 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -7,7 +7,6 @@ indexes: - name: product - name: serial - name: status - - name: scheduling_status - kind: BuildModel ancestor: no @@ -17,59 +16,27 @@ indexes: direction: desc - name: build_target - name: build_type - - name: artifact_type - - name: artifacts -- kind: ScheduleModel - ancestor: no - properties: - - name: manifest_branch - - name: build_target - - name: test_name - - name: require_signed_device_build - - name: period - - name: priority - - name: device - - name: shards - - name: param - - name: retry_count - - name: gsi_branch - - name: gsi_build_target - - name: gsi_pab_account_id - - name: test_branch - - name: test_build_target - - name: test_pab_account_id +#- kind: ScheduleModel +# ancestor: no +# properties: +# - name: schedule_type -- kind: LabModel - ancestor: no - properties: - - name: name - - name: owner - - name: hostname - - name: ip - - name: script - - name: devices +#- kind: LabModel +# ancestor: no +# properties: +# - name: name - kind: JobModel ancestor: no properties: - name: hostname - - name: priority - - name: period - - name: retry_count - name: test_name - - name: device - - name: serial - name: manifest_branch - name: build_target - name: shards - - name: param - - name: build_id - name: status + - name: period - name: gsi_branch - - name: gsi_build_target - - name: gsi_pab_account_id - name: test_branch - - name: test_build_target - - name: test_pab_account_id - - name: infra_log_url + - name: retry_count diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 803feed..4d2a11f 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -26,10 +26,10 @@ class BuildModel(ndb.Model): build_id = ndb.StringProperty() build_target = ndb.StringProperty() build_type = ndb.StringProperty() - artifact_type = ndb.StringProperty() - artifacts = ndb.StringProperty(repeated=True) - timestamp = ndb.DateTimeProperty(auto_now=False) - signed = ndb.BooleanProperty() + artifact_type = ndb.StringProperty(indexed=False) + artifacts = ndb.StringProperty(repeated=True, indexed=False) + timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) + signed = ndb.BooleanProperty(indexed=False) class BuildInfoMessage(messages.Message): @@ -46,37 +46,37 @@ class BuildInfoMessage(messages.Message): class ScheduleModel(ndb.Model): """A model for representing an individual schedule entry.""" # schedule name for green build schedule, optional. - name = ndb.StringProperty() + name = ndb.StringProperty(indexed=False) schedule_type = ndb.StringProperty() # device image information - build_storage_type = ndb.IntegerProperty() - manifest_branch = ndb.StringProperty() - build_target = ndb.StringProperty() # type:name - device_pab_account_id = ndb.StringProperty() - require_signed_device_build = ndb.BooleanProperty() + build_storage_type = ndb.IntegerProperty(indexed=False) + manifest_branch = ndb.StringProperty(indexed=False) + build_target = ndb.StringProperty(indexed=False) # type:name + device_pab_account_id = ndb.StringProperty(indexed=False) + require_signed_device_build = ndb.BooleanProperty(indexed=False) # GSI information - gsi_storage_type = ndb.IntegerProperty() - gsi_branch = ndb.StringProperty() - gsi_build_target = ndb.StringProperty() - gsi_pab_account_id = ndb.StringProperty() + gsi_storage_type = ndb.IntegerProperty(indexed=False) + gsi_branch = ndb.StringProperty(indexed=False) + gsi_build_target = ndb.StringProperty(indexed=False) + gsi_pab_account_id = ndb.StringProperty(indexed=False) # test suite information - test_storage_type = ndb.IntegerProperty() - test_branch = ndb.StringProperty() - test_build_target = ndb.StringProperty() - test_pab_account_id = ndb.StringProperty() - - test_name = ndb.StringProperty() - period = ndb.IntegerProperty() - schedule = ndb.StringProperty() - priority = ndb.StringProperty() - device = ndb.StringProperty(repeated=True) - shards = ndb.IntegerProperty() - param = ndb.StringProperty(repeated=True) - timestamp = ndb.DateTimeProperty(auto_now=False) - retry_count = ndb.IntegerProperty() + test_storage_type = ndb.IntegerProperty(indexed=False) + test_branch = ndb.StringProperty(indexed=False) + test_build_target = ndb.StringProperty(indexed=False) + test_pab_account_id = ndb.StringProperty(indexed=False) + + test_name = ndb.StringProperty(indexed=False) + period = ndb.IntegerProperty(indexed=False) + schedule = ndb.StringProperty(indexed=False) + priority = ndb.StringProperty(indexed=False) + device = ndb.StringProperty(repeated=True, indexed=False) + shards = ndb.IntegerProperty(indexed=False) + param = ndb.StringProperty(repeated=True, indexed=False) + timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) + retry_count = ndb.IntegerProperty(indexed=False) class ScheduleInfoMessage(messages.Message): @@ -117,12 +117,12 @@ class ScheduleInfoMessage(messages.Message): class LabModel(ndb.Model): """A model for representing an individual lab entry.""" name = ndb.StringProperty() - owner = ndb.StringProperty() - hostname = ndb.StringProperty() - ip = ndb.StringProperty() + owner = ndb.StringProperty(indexed=False) + hostname = ndb.StringProperty(indexed=False) + ip = ndb.StringProperty(indexed=False) # devices is a comma-separated list of serial=product pairs - devices = ndb.StringProperty() - timestamp = ndb.DateTimeProperty(auto_now=False) + devices = ndb.StringProperty(indexed=False) + timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) class LabDeviceInfoMessage(messages.Message): @@ -154,8 +154,8 @@ class DeviceModel(ndb.Model): product = ndb.StringProperty() serial = ndb.StringProperty() status = ndb.IntegerProperty() - scheduling_status = ndb.IntegerProperty() - timestamp = ndb.DateTimeProperty(auto_now=False) + scheduling_status = ndb.IntegerProperty(indexed=False) + timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) class DeviceInfoMessage(messages.Message): @@ -176,43 +176,43 @@ class HostInfoMessage(messages.Message): class JobModel(ndb.Model): """A model for representing an individual job entry.""" hostname = ndb.StringProperty() - priority = ndb.StringProperty() + priority = ndb.StringProperty(indexed=False) test_name = ndb.StringProperty() - require_signed_device_build = ndb.BooleanProperty() - device = ndb.StringProperty() - serial = ndb.StringProperty(repeated=True) + require_signed_device_build = ndb.BooleanProperty(indexed=False) + device = ndb.StringProperty(indexed=False) + serial = ndb.StringProperty(repeated=True, indexed=False) # device image information - build_storage_type = ndb.IntegerProperty() + build_storage_type = ndb.IntegerProperty(indexed=False) manifest_branch = ndb.StringProperty() build_target = ndb.StringProperty() - build_id = ndb.StringProperty() - pab_account_id = ndb.StringProperty() + build_id = ndb.StringProperty(indexed=False) + pab_account_id = ndb.StringProperty(indexed=False) shards = ndb.IntegerProperty() - param = ndb.StringProperty(repeated=True) + param = ndb.StringProperty(repeated=True, indexed=False) status = ndb.IntegerProperty() period = ndb.IntegerProperty() # GSI information - gsi_storage_type = ndb.IntegerProperty() + gsi_storage_type = ndb.IntegerProperty(indexed=False) gsi_branch = ndb.StringProperty() - gsi_build_target = ndb.StringProperty() - gsi_build_id = ndb.StringProperty() - gsi_pab_account_id = ndb.StringProperty() + gsi_build_target = ndb.StringProperty(indexed=False) + gsi_build_id = ndb.StringProperty(indexed=False) + gsi_pab_account_id = ndb.StringProperty(indexed=False) # test suite information - test_storage_type = ndb.IntegerProperty() + test_storage_type = ndb.IntegerProperty(indexed=False) test_branch = ndb.StringProperty() - test_build_target = ndb.StringProperty() - test_build_id = ndb.StringProperty() - test_pab_account_id = ndb.StringProperty() + test_build_target = ndb.StringProperty(indexed=False) + test_build_id = ndb.StringProperty(indexed=False) + test_pab_account_id = ndb.StringProperty(indexed=False) - timestamp = ndb.DateTimeProperty(auto_now=False) - heartbeat_stamp = ndb.DateTimeProperty(auto_now=False) + timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) + heartbeat_stamp = ndb.DateTimeProperty(auto_now=False, indexed=False) retry_count = ndb.IntegerProperty() - infra_log_url = ndb.StringProperty() + infra_log_url = ndb.StringProperty(indexed=False) class JobMessage(messages.Message): -- cgit v1.2.3 From 7a0169e6765adff08e507d73bdcfd6111deb07cd Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Thu, 5 Apr 2018 15:10:09 -0700 Subject: add a scheduler suspenion button Test: on GAE Bug: 77652496 Change-Id: I3cc8fd6d30961b9a5735c9f2cd7c3bed854e1923 --- gae/webapp/src/dashboard/schedule_list.py | 26 +++++++++++++++++++++++++- gae/webapp/src/proto/model.py | 13 +++++++++++++ gae/webapp/src/scheduler/periodic.py | 28 +++++++++++++++++++++------- gae/webapp/static/schedule.html | 2 ++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/gae/webapp/src/dashboard/schedule_list.py b/gae/webapp/src/dashboard/schedule_list.py index 0f00b79..627ca3c 100644 --- a/gae/webapp/src/dashboard/schedule_list.py +++ b/gae/webapp/src/dashboard/schedule_list.py @@ -26,6 +26,29 @@ class SchedulePage(BaseHandler): """Generates an HTML page based on the task schedules kept in DB.""" self.template = "schedule.html" + toggle = self.request.get("schedule_enable_status_toggle", default_value="0") + + schedule_control = model.ScheduleControlModel.query() + schedule_control_dataset = schedule_control.fetch() + enabled = True + if schedule_control_dataset: + for schedule_control_data_tuple in schedule_control_dataset: + if (not schedule_control_data_tuple.schedule_name or + schedule_control_data_tuple.schedule_name == "global"): + enabled = schedule_control_data_tuple.enabled + if toggle == "1": + enabled = not enabled + schedule_control_data_tuple.enabled = enabled + schedule_control_data_tuple.put() + toggle = "0" + break + + if toggle == "1": + schedule_control_data_tuple = model.ScheduleControlModel() + enabled = not enabled + schedule_control_data_tuple.enabled = enabled + schedule_control_data_tuple.put() + schedule_query = model.ScheduleModel.query() schedules = schedule_query.fetch() @@ -36,6 +59,7 @@ class SchedulePage(BaseHandler): template_values = { "schedules": schedules, + "enabled": enabled } - self.render(template_values) \ No newline at end of file + self.render(template_values) diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 4d2a11f..a74f330 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -43,6 +43,13 @@ class BuildInfoMessage(messages.Message): signed = messages.BooleanField(7) +class ScheduleControlModel(ndb.Model): + """A model for representing a schedule control data entry.""" + enabled = ndb.BooleanProperty() + # "global" or empty string to enable/disable all schedules. + schedule_name = ndb.StringProperty() + + class ScheduleModel(ndb.Model): """A model for representing an individual schedule entry.""" # schedule name for green build schedule, optional. @@ -79,6 +86,12 @@ class ScheduleModel(ndb.Model): retry_count = ndb.IntegerProperty(indexed=False) +class ScheduleControlInfoMessage(messages.Message): + """A message for representing a schedule control data entry.""" + enabled = messages.BooleanField(1) + schedule_name = messages.StringField(2) + + class ScheduleInfoMessage(messages.Message): """A message for representing an individual schedule entry.""" # schedule name for green build schedule, optional. diff --git a/gae/webapp/src/scheduler/periodic.py b/gae/webapp/src/scheduler/periodic.py index 9d48748..ce32e96 100644 --- a/gae/webapp/src/scheduler/periodic.py +++ b/gae/webapp/src/scheduler/periodic.py @@ -99,6 +99,20 @@ class PeriodicScheduler(webapp2.RequestHandler): """Generates an HTML page based on the task schedules kept in DB.""" self.logger.Clear() + schedule_control = model.ScheduleControlModel.query() + schedule_control_dataset = schedule_control.fetch() + enabled = True + if schedule_control_dataset: + for schedule_control_data_tuple in schedule_control_dataset: + if (not schedule_control_data_tuple.schedule_name or + schedule_control_data_tuple.schedule_name == "global"): + enabled = schedule_control_data_tuple.enabled + + if not enabled: + self.response.write( + "
\nScheduler not enabled.\n
") + return + schedule_query = model.ScheduleModel.query() schedules = schedule_query.fetch() @@ -108,7 +122,8 @@ class PeriodicScheduler(webapp2.RequestHandler): self.logger.Println("Schedule: %s (branch: %s)" % (schedule.test_name, schedule.manifest_branch)) - self.logger.Println("Device: %s" % schedule.build_target) + self.logger.Println("Build Target: %s" % schedule.build_target) + self.logger.Println("Device: %s" % schedule.device) self.logger.Indent() if not self.NewPeriod(schedule): self.logger.Println("- Skipped") @@ -267,8 +282,7 @@ class PeriodicScheduler(webapp2.RequestHandler): continue target_lab, target_product_type = target_device.split("/") - self.logger.Println("- Lab %s (device: %s)" % - (target_lab, target_product_type)) + self.logger.Println("- Lab %s" % target_lab) self.logger.Indent() lab_query = model.LabModel.query(model.LabModel.name == target_lab) target_labs = lab_query.fetch() @@ -276,8 +290,7 @@ class PeriodicScheduler(webapp2.RequestHandler): available_devices = {} if target_labs: for lab in target_labs: - self.logger.Println("- Host: %s (device: %s)" % - (lab.hostname, target_product_type)) + self.logger.Println("- Host: %s" % lab.hostname) self.logger.Indent() device_query = model.DeviceModel.query( model.DeviceModel.hostname == lab.hostname) @@ -307,7 +320,8 @@ class PeriodicScheduler(webapp2.RequestHandler): return host, target_device, list( available_devices[host])[:schedule.shards] self.logger.Println( - "- Not enough devices found, while %s required." % ( - schedule.shards)) + "- Not enough devices found, while %s required.\n%s" % ( + schedule.shards, available_devices)) + self.logger.Unindent() self.logger.Unindent() return None, None, [] diff --git a/gae/webapp/static/schedule.html b/gae/webapp/static/schedule.html index 06cd834..4fcc6e3 100644 --- a/gae/webapp/static/schedule.html +++ b/gae/webapp/static/schedule.html @@ -67,6 +67,8 @@
+
Scheduler Enabled: {{ enabled }} ( + Toggle)
# -- cgit v1.2.3 From cbfc189dd092fb0533d38417abe5ef81e0f79bf9 Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Thu, 5 Apr 2018 15:26:17 -0700 Subject: cloud scheduler change to support gsi_vendor_version flag Test: mma Bug: 69176190 Change-Id: I4b5e7e025a9c62beb411bb982d4aa03c42662a56 --- gae/webapp/src/endpoint/job_queue.py | 1 + gae/webapp/src/endpoint/schedule_info.py | 2 ++ gae/webapp/src/proto/model.py | 5 +++++ gae/webapp/src/scheduler/periodic.py | 1 + 4 files changed, 9 insertions(+) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 32a717e..ea18634 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -93,6 +93,7 @@ class JobQueueApi(remote.Service): job_message.gsi_build_target = job.gsi_build_target job_message.gsi_build_id = job.gsi_build_id job_message.gsi_pab_account_id = job.gsi_pab_account_id + job_message.gsi_vendor_version = job.gsi_vendor_version job_message.test_storage_type = job.test_storage_type job_message.test_branch = job.test_branch job_message.test_build_target = job.test_build_target diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index fc8340b..4ec084e 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -74,6 +74,7 @@ class ScheduleInfoApi(remote.Service): schedule.gsi_storage_type = request.gsi_storage_type schedule.gsi_branch = request.gsi_branch schedule.gsi_build_target = request.gsi_build_target + schedule.gsi_vendor_version = request.gsi_vendor_version schedule.gsi_pab_account_id = request.gsi_pab_account_id schedule.test_storage_type = request.test_storage_type schedule.test_branch = request.test_branch @@ -128,6 +129,7 @@ class GreenScheduleInfoApi(remote.Service): schedule.gsi_branch = request.gsi_branch schedule.gsi_build_target = request.gsi_build_target schedule.gsi_pab_account_id = request.gsi_pab_account_id + schedule.gsi_vendor_version = request.gsi_vendor_version schedule.test_branch = request.test_branch schedule.test_build_target = request.test_build_target schedule.test_pab_account_id = request.test_pab_account_id diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index a74f330..f8c1062 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -68,6 +68,7 @@ class ScheduleModel(ndb.Model): gsi_branch = ndb.StringProperty(indexed=False) gsi_build_target = ndb.StringProperty(indexed=False) gsi_pab_account_id = ndb.StringProperty(indexed=False) + gsi_vendor_version = ndb.StringProperty(indexed=False) # test suite information test_storage_type = ndb.IntegerProperty(indexed=False) @@ -94,6 +95,7 @@ class ScheduleControlInfoMessage(messages.Message): class ScheduleInfoMessage(messages.Message): """A message for representing an individual schedule entry.""" + # Next ID = 25 # schedule name for green build schedule, optional. name = messages.StringField(16) schedule_type = messages.StringField(19) @@ -110,6 +112,7 @@ class ScheduleInfoMessage(messages.Message): gsi_branch = messages.StringField(9) gsi_build_target = messages.StringField(10) gsi_pab_account_id = messages.StringField(11) + gsi_vendor_version = messages.StringField(24) # test suite information test_storage_type = messages.IntegerField(23) @@ -213,6 +216,7 @@ class JobModel(ndb.Model): gsi_build_target = ndb.StringProperty(indexed=False) gsi_build_id = ndb.StringProperty(indexed=False) gsi_pab_account_id = ndb.StringProperty(indexed=False) + gsi_vendor_version = ndb.StringProperty(indexed=False) # test suite information test_storage_type = ndb.IntegerProperty(indexed=False) @@ -255,6 +259,7 @@ class JobMessage(messages.Message): gsi_build_target = messages.StringField(14) gsi_build_id = messages.StringField(21) gsi_pab_account_id = messages.StringField(15) + gsi_vendor_version = messages.StringField(28) # test suite information test_storage_type = messages.IntegerField(27) diff --git a/gae/webapp/src/scheduler/periodic.py b/gae/webapp/src/scheduler/periodic.py index ce32e96..b93f386 100644 --- a/gae/webapp/src/scheduler/periodic.py +++ b/gae/webapp/src/scheduler/periodic.py @@ -162,6 +162,7 @@ class PeriodicScheduler(webapp2.RequestHandler): new_job.gsi_branch = schedule.gsi_branch new_job.gsi_build_target = schedule.gsi_build_target new_job.gsi_pab_account_id = schedule.gsi_pab_account_id + new_job.gsi_vendor_version = schedule.gsi_vendor_version new_job.test_storage_type = schedule.test_storage_type new_job.test_branch = schedule.test_branch new_job.test_build_target = schedule.test_build_target -- cgit v1.2.3 From aea8b1d7288732d2537fc525f8c7f14a06cacf17 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 9 Apr 2018 23:28:22 +0900 Subject: Fix ZeroDivisionError Test: vtslab-schedule-dev.appspot.com/job Bug: 75325573 --- gae/webapp/static/job.html | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index b1ac3bb..957fee8 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -83,15 +83,45 @@
24 Hours {{ stats_24hrs.created }} - {{ stats_24hrs.completed * 100 / stats_24hrs.created }}% - {{ (stats_24hrs.running + stats_24hrs.ready) * 100 / stats_24hrs.created }}% - {{ (stats_24hrs.failed + stats_24hrs.expired) * 100 / stats_24hrs.created }}% + + {% if stats_24hrs.created %} + {{ stats_24hrs.completed * 100 / stats_24hrs.created }}% + {% else %} + n/a + {% endif %} + + {% if stats_24hrs.created %} + {{ (stats_24hrs.running + stats_24hrs.ready) * 100 / stats_24hrs.created }}% + {% else %} + n/a + {% endif %} + + {% if stats_24hrs.created %} + {{ (stats_24hrs.failed + stats_24hrs.expired) * 100 / stats_24hrs.created }}% + {% else %} + n/a + {% endif %}
All {{ stats_all.created }} - {{ stats_all.completed * 100 / stats_all.created }}% - {{ (stats_all.running + stats_all.ready) * 100 / stats_all.created }}% - {{ (stats_all.failed + stats_all.expired) * 100 / stats_all.created }}% + + {% if stats_all.created %} + {{ stats_all.completed * 100 / stats_all.created }}% + {% else %} + n/a + {% endif %} + + {% if stats_all.created %} + {{ (stats_all.running + stats_all.ready) * 100 / stats_all.created }}% + {% else %} + n/a + {% endif %} + + {% if stats_all.created %} + {{ (stats_all.failed + stats_all.expired) * 100 / stats_all.created }}% + {% else %} + n/a + {% endif %}

-- cgit v1.2.3 From 4cda5409ab2296101c00f944d415ceb717cc807e Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Fri, 6 Apr 2018 16:18:26 +0000 Subject: Revert "Reduced the numbers of indexed fields." This reverts commit a0e13220473505a0d1ee4cd786611abe81a2b5ec. Test: mma Change-Id: I34db696241faade559dfed12e43846f3d0f67c14 --- gae/index.yaml | 53 ++++++++++++++++---- gae/webapp/src/proto/model.py | 112 +++++++++++++++++++++--------------------- 2 files changed, 99 insertions(+), 66 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index e8c2d0d..6b9e317 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -7,6 +7,7 @@ indexes: - name: product - name: serial - name: status + - name: scheduling_status - kind: BuildModel ancestor: no @@ -16,27 +17,59 @@ indexes: direction: desc - name: build_target - name: build_type + - name: artifact_type + - name: artifacts -#- kind: ScheduleModel -# ancestor: no -# properties: -# - name: schedule_type +- kind: ScheduleModel + ancestor: no + properties: + - name: manifest_branch + - name: build_target + - name: test_name + - name: require_signed_device_build + - name: period + - name: priority + - name: device + - name: shards + - name: param + - name: retry_count + - name: gsi_branch + - name: gsi_build_target + - name: gsi_pab_account_id + - name: test_branch + - name: test_build_target + - name: test_pab_account_id -#- kind: LabModel -# ancestor: no -# properties: -# - name: name +- kind: LabModel + ancestor: no + properties: + - name: name + - name: owner + - name: hostname + - name: ip + - name: script + - name: devices - kind: JobModel ancestor: no properties: - name: hostname + - name: priority + - name: period + - name: retry_count - name: test_name + - name: device + - name: serial - name: manifest_branch - name: build_target - name: shards + - name: param + - name: build_id - name: status - - name: period - name: gsi_branch + - name: gsi_build_target + - name: gsi_pab_account_id - name: test_branch - - name: retry_count + - name: test_build_target + - name: test_pab_account_id + - name: infra_log_url diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index f8c1062..6370f10 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -26,10 +26,10 @@ class BuildModel(ndb.Model): build_id = ndb.StringProperty() build_target = ndb.StringProperty() build_type = ndb.StringProperty() - artifact_type = ndb.StringProperty(indexed=False) - artifacts = ndb.StringProperty(repeated=True, indexed=False) - timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) - signed = ndb.BooleanProperty(indexed=False) + artifact_type = ndb.StringProperty() + artifacts = ndb.StringProperty(repeated=True) + timestamp = ndb.DateTimeProperty(auto_now=False) + signed = ndb.BooleanProperty() class BuildInfoMessage(messages.Message): @@ -53,38 +53,38 @@ class ScheduleControlModel(ndb.Model): class ScheduleModel(ndb.Model): """A model for representing an individual schedule entry.""" # schedule name for green build schedule, optional. - name = ndb.StringProperty(indexed=False) + name = ndb.StringProperty() schedule_type = ndb.StringProperty() # device image information - build_storage_type = ndb.IntegerProperty(indexed=False) - manifest_branch = ndb.StringProperty(indexed=False) - build_target = ndb.StringProperty(indexed=False) # type:name - device_pab_account_id = ndb.StringProperty(indexed=False) - require_signed_device_build = ndb.BooleanProperty(indexed=False) + build_storage_type = ndb.IntegerProperty() + manifest_branch = ndb.StringProperty() + build_target = ndb.StringProperty() # type:name + device_pab_account_id = ndb.StringProperty() + require_signed_device_build = ndb.BooleanProperty() # GSI information - gsi_storage_type = ndb.IntegerProperty(indexed=False) - gsi_branch = ndb.StringProperty(indexed=False) - gsi_build_target = ndb.StringProperty(indexed=False) - gsi_pab_account_id = ndb.StringProperty(indexed=False) - gsi_vendor_version = ndb.StringProperty(indexed=False) + gsi_storage_type = ndb.IntegerProperty() + gsi_branch = ndb.StringProperty() + gsi_build_target = ndb.StringProperty() + gsi_pab_account_id = ndb.StringProperty() + gsi_vendor_version = ndb.StringProperty() # test suite information - test_storage_type = ndb.IntegerProperty(indexed=False) - test_branch = ndb.StringProperty(indexed=False) - test_build_target = ndb.StringProperty(indexed=False) - test_pab_account_id = ndb.StringProperty(indexed=False) - - test_name = ndb.StringProperty(indexed=False) - period = ndb.IntegerProperty(indexed=False) - schedule = ndb.StringProperty(indexed=False) - priority = ndb.StringProperty(indexed=False) - device = ndb.StringProperty(repeated=True, indexed=False) - shards = ndb.IntegerProperty(indexed=False) - param = ndb.StringProperty(repeated=True, indexed=False) - timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) - retry_count = ndb.IntegerProperty(indexed=False) + test_storage_type = ndb.IntegerProperty() + test_branch = ndb.StringProperty() + test_build_target = ndb.StringProperty() + test_pab_account_id = ndb.StringProperty() + + test_name = ndb.StringProperty() + period = ndb.IntegerProperty() + schedule = ndb.StringProperty() + priority = ndb.StringProperty() + device = ndb.StringProperty(repeated=True) + shards = ndb.IntegerProperty() + param = ndb.StringProperty(repeated=True) + timestamp = ndb.DateTimeProperty(auto_now=False) + retry_count = ndb.IntegerProperty() class ScheduleControlInfoMessage(messages.Message): @@ -133,12 +133,12 @@ class ScheduleInfoMessage(messages.Message): class LabModel(ndb.Model): """A model for representing an individual lab entry.""" name = ndb.StringProperty() - owner = ndb.StringProperty(indexed=False) - hostname = ndb.StringProperty(indexed=False) - ip = ndb.StringProperty(indexed=False) + owner = ndb.StringProperty() + hostname = ndb.StringProperty() + ip = ndb.StringProperty() # devices is a comma-separated list of serial=product pairs - devices = ndb.StringProperty(indexed=False) - timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) + devices = ndb.StringProperty() + timestamp = ndb.DateTimeProperty(auto_now=False) class LabDeviceInfoMessage(messages.Message): @@ -170,8 +170,8 @@ class DeviceModel(ndb.Model): product = ndb.StringProperty() serial = ndb.StringProperty() status = ndb.IntegerProperty() - scheduling_status = ndb.IntegerProperty(indexed=False) - timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) + scheduling_status = ndb.IntegerProperty() + timestamp = ndb.DateTimeProperty(auto_now=False) class DeviceInfoMessage(messages.Message): @@ -192,44 +192,44 @@ class HostInfoMessage(messages.Message): class JobModel(ndb.Model): """A model for representing an individual job entry.""" hostname = ndb.StringProperty() - priority = ndb.StringProperty(indexed=False) + priority = ndb.StringProperty() test_name = ndb.StringProperty() - require_signed_device_build = ndb.BooleanProperty(indexed=False) - device = ndb.StringProperty(indexed=False) - serial = ndb.StringProperty(repeated=True, indexed=False) + require_signed_device_build = ndb.BooleanProperty() + device = ndb.StringProperty() + serial = ndb.StringProperty(repeated=True) # device image information - build_storage_type = ndb.IntegerProperty(indexed=False) + build_storage_type = ndb.IntegerProperty() manifest_branch = ndb.StringProperty() build_target = ndb.StringProperty() - build_id = ndb.StringProperty(indexed=False) - pab_account_id = ndb.StringProperty(indexed=False) + build_id = ndb.StringProperty() + pab_account_id = ndb.StringProperty() shards = ndb.IntegerProperty() - param = ndb.StringProperty(repeated=True, indexed=False) + param = ndb.StringProperty(repeated=True) status = ndb.IntegerProperty() period = ndb.IntegerProperty() # GSI information - gsi_storage_type = ndb.IntegerProperty(indexed=False) + gsi_storage_type = ndb.IntegerProperty() gsi_branch = ndb.StringProperty() - gsi_build_target = ndb.StringProperty(indexed=False) - gsi_build_id = ndb.StringProperty(indexed=False) - gsi_pab_account_id = ndb.StringProperty(indexed=False) - gsi_vendor_version = ndb.StringProperty(indexed=False) + gsi_build_target = ndb.StringProperty() + gsi_build_id = ndb.StringProperty() + gsi_pab_account_id = ndb.StringProperty() + gsi_vendor_version = ndb.StringProperty() # test suite information - test_storage_type = ndb.IntegerProperty(indexed=False) + test_storage_type = ndb.IntegerProperty() test_branch = ndb.StringProperty() - test_build_target = ndb.StringProperty(indexed=False) - test_build_id = ndb.StringProperty(indexed=False) - test_pab_account_id = ndb.StringProperty(indexed=False) + test_build_target = ndb.StringProperty() + test_build_id = ndb.StringProperty() + test_pab_account_id = ndb.StringProperty() - timestamp = ndb.DateTimeProperty(auto_now=False, indexed=False) - heartbeat_stamp = ndb.DateTimeProperty(auto_now=False, indexed=False) + timestamp = ndb.DateTimeProperty(auto_now=False) + heartbeat_stamp = ndb.DateTimeProperty(auto_now=False) retry_count = ndb.IntegerProperty() - infra_log_url = ndb.StringProperty(indexed=False) + infra_log_url = ndb.StringProperty() class JobMessage(messages.Message): -- cgit v1.2.3 From f9eac9c3308e6441edd7a934057af450bbf40908 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 10 Apr 2018 13:05:47 +0900 Subject: Add backend service for scheduling Test: vtslab-schedule-dev Bug: 77618305 --- gae/queue.yaml | 9 + gae/webapp/src/scheduler/periodic.py | 296 +---------------------- gae/webapp/src/scheduler/periodic_test.py | 108 --------- gae/webapp/src/scheduler/schedule_worker.py | 278 +++++++++++++++++++++ gae/webapp/src/scheduler/schedule_worker_test.py | 108 +++++++++ gae/webapp/src/worker_main.py | 23 ++ gae/worker.yaml | 9 + 7 files changed, 438 insertions(+), 393 deletions(-) create mode 100644 gae/queue.yaml delete mode 100644 gae/webapp/src/scheduler/periodic_test.py create mode 100644 gae/webapp/src/scheduler/schedule_worker.py create mode 100644 gae/webapp/src/scheduler/schedule_worker_test.py create mode 100644 gae/webapp/src/worker_main.py create mode 100644 gae/worker.yaml diff --git a/gae/queue.yaml b/gae/queue.yaml new file mode 100644 index 0000000..87bcebf --- /dev/null +++ b/gae/queue.yaml @@ -0,0 +1,9 @@ +queue: +- name: queue-schedule + mode: push + rate: 1/s + bucket_size: 5 + max_concurrent_requests: 1 + retry_parameters: + task_retry_limit: 2 + min_backoff_seconds: 1 \ No newline at end of file diff --git a/gae/webapp/src/scheduler/periodic.py b/gae/webapp/src/scheduler/periodic.py index b93f386..627ec14 100644 --- a/gae/webapp/src/scheduler/periodic.py +++ b/gae/webapp/src/scheduler/periodic.py @@ -14,91 +14,21 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -import datetime -import logging import webapp2 -from webapp.src import vtslab_status as Status from webapp.src.proto import model -from webapp.src.utils import logger - -def StrGT(left, right): - """Returns true if `left` string is greater than `right` in value.""" - if len(left) > len(right): - right = "0" * (len(left) - len(right)) + right - elif len(right) > len(left): - left = "0" * (len(right) - len(left)) + left - return left > right +from google.appengine.api import taskqueue class PeriodicScheduler(webapp2.RequestHandler): """Main class for /tasks/schedule servlet. - This class creates jobs from registered schedules periodically. - - Attributes: - logger: Logger class + This class creates a task, which creates schedules, in given period. """ - logger = logger.Logger() - - def ReserveDevices(self, target_device_serials): - """Reserves devices. - - Args: - target_device_serials: a list of strings, containing target device - serial numbers. - """ - device_query = model.DeviceModel.query( - model.DeviceModel.serial.IN(target_device_serials)) - devices = device_query.fetch() - for device in devices: - device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ - "reserved"] - device.put() - - def FindBuildId(self, new_job): - """Finds build ID for a new job. - - Args: - new_job: JobModel, a new job. - - Return: - string, build ID found. - """ - build_id = "" - build_query = model.BuildModel.query( - model.BuildModel.manifest_branch == new_job.manifest_branch) - builds = build_query.fetch() - - if builds: - self.logger.Println("-- Find build ID") - # Remove builds if build_id info is none - build_id_filled = [x for x in builds if x.build_id] - sorted_list = sorted( - build_id_filled, key=lambda x: int(x.build_id), reverse=True) - filtered_list = [ - x for x in sorted_list - if (all( - hasattr(x, attrs) - for attrs in ["build_target", "build_type", "build_id"]) - and x.build_target and x.build_type) - ] - for device_build in filtered_list: - candidate_build_target = "-".join( - [device_build.build_target, device_build.build_type]) - if (new_job.build_target == candidate_build_target and - (not new_job.require_signed_device_build or - device_build.signed)): - build_id = device_build.build_id - break - return build_id def get(self): - """Generates an HTML page based on the task schedules kept in DB.""" - self.logger.Clear() - + """Enqueues a scheduling task if scheduler is enabled.""" schedule_control = model.ScheduleControlModel.query() schedule_control_dataset = schedule_control.fetch() enabled = True @@ -113,216 +43,12 @@ class PeriodicScheduler(webapp2.RequestHandler): "
\nScheduler not enabled.\n
") return - schedule_query = model.ScheduleModel.query() - schedules = schedule_query.fetch() - - if schedules: - for schedule in schedules: - self.logger.Println("") - self.logger.Println("Schedule: %s (branch: %s)" % - (schedule.test_name, - schedule.manifest_branch)) - self.logger.Println("Build Target: %s" % schedule.build_target) - self.logger.Println("Device: %s" % schedule.device) - self.logger.Indent() - if not self.NewPeriod(schedule): - self.logger.Println("- Skipped") - self.logger.Unindent() - continue - - target_host, target_device, target_device_serials = ( - self.SelectTargetLab(schedule)) - if not target_host: - self.logger.Unindent() - continue - - self.logger.Println("- Target host: %s" % target_host) - self.logger.Println("- Target device: %s" % target_device) - self.logger.Println( - "- Target serials: %s" % target_device_serials) - # TODO: update device status - - # create job and add. - new_job = model.JobModel() - new_job.hostname = target_host - new_job.priority = schedule.priority - new_job.test_name = schedule.test_name - new_job.require_signed_device_build = ( - schedule.require_signed_device_build) - new_job.device = target_device - new_job.period = schedule.period - new_job.serial.extend(target_device_serials) - new_job.build_storage_type = schedule.build_storage_type - new_job.manifest_branch = schedule.manifest_branch - new_job.build_target = schedule.build_target - new_job.shards = schedule.shards - new_job.param = schedule.param - new_job.retry_count = schedule.retry_count - new_job.gsi_storage_type = schedule.gsi_storage_type - new_job.gsi_branch = schedule.gsi_branch - new_job.gsi_build_target = schedule.gsi_build_target - new_job.gsi_pab_account_id = schedule.gsi_pab_account_id - new_job.gsi_vendor_version = schedule.gsi_vendor_version - new_job.test_storage_type = schedule.test_storage_type - new_job.test_branch = schedule.test_branch - new_job.test_build_target = schedule.test_build_target - new_job.test_pab_account_id = ( - schedule.test_pab_account_id) - - new_job.build_id = "" - - if new_job.build_storage_type == ( - Status.STORAGE_TYPE_DICT["PAB"]): - new_job.build_id = self.FindBuildId(new_job) - if new_job.build_id: - self.ReserveDevices(target_device_serials) - new_job.status = Status.JOB_STATUS_DICT[ - "ready"] - new_job.timestamp = datetime.datetime.now() - new_job.put() - self.logger.Println("NEW JOB") - else: - self.logger.Println("NO BUILD FOUND") - elif new_job.build_storage_type == ( - Status.STORAGE_TYPE_DICT["GCS"]): - new_job.status = Status.JOB_STATUS_DICT["ready"] - new_job.timestamp = datetime.datetime.now() - new_job.put() - self.logger.Println("NEW JOB - GCS") - else: - self.logger.Println("Unexpected storage type (%s)." % - new_job.build_storage_type) - - self.logger.Unindent() - + task = taskqueue.add( + url="/worker/schedule_handler", + target="worker", + queue_name="queue-schedule", + transactional=False + ) self.response.write( - "
\n" + "\n".join(self.logger.Get()) + "\n
") - - def NewPeriod(self, schedule): - """Checks whether a new job creation is needed. - - Args: - schedule: a proto containing schedule information. - - Returns: - True if new job is required, False otherwise. - """ - job_query = model.JobModel.query( - model.JobModel.manifest_branch == schedule.manifest_branch, - model.JobModel.build_target == schedule.build_target, - model.JobModel.test_name == schedule.test_name, - model.JobModel.period == schedule.period, - model.JobModel.shards == schedule.shards, - model.JobModel.retry_count == schedule.retry_count, - model.JobModel.gsi_branch == schedule.gsi_branch, - model.JobModel.test_branch == schedule.test_branch) - same_jobs = job_query.fetch() - same_jobs = [ - x for x in same_jobs - if (set(x.param) == set(schedule.param) - and x.device in schedule.device) - ] - if not same_jobs: - return True - - outdated_jobs = [ - x for x in same_jobs - if (datetime.datetime.now() - x.timestamp > datetime.timedelta( - minutes=x.period)) - ] - outdated_ready_jobs = [ - x for x in outdated_jobs - if x.status == Status.JOB_STATUS_DICT["expired"] - ] - - if outdated_ready_jobs: - msg = ("Job key[{}] is(are) outdated. " - "They became infra-err status.").format( - ", ".join( - [str(x.key.id()) for x in outdated_ready_jobs])) - logging.debug(msg) - self.logger.Println(msg) - for job in outdated_ready_jobs: - job.status = Status.JOB_STATUS_DICT["infra-err"] - job.put() - - outdated_leased_jobs = [ - x for x in outdated_jobs - if x.status == Status.JOB_STATUS_DICT["leased"] - ] - if outdated_leased_jobs: - msg = ("Job key[{}] is(are) expected to be completed " - "however still in leased status.").format( - ", ".join( - [str(x.key.id()) for x in outdated_leased_jobs])) - logging.debug(msg) - self.logger.Println(msg) - - recent_jobs = [x for x in same_jobs if x not in outdated_jobs] - - if recent_jobs or outdated_leased_jobs: - return False - else: - return True - - def SelectTargetLab(self, schedule): - """Find target host and devices to schedule a new job. - - Args: - schedule: a proto containing the information of a schedule. - - Returns: - a string which represents hostname, - a string containing target lab and product with '/' separator, - a list of selected devices serial (see whether devices will be - selected later when the job is picked up.) - """ - for target_device in schedule.device: - if "/" not in target_device: - # device malformed - continue - - target_lab, target_product_type = target_device.split("/") - self.logger.Println("- Lab %s" % target_lab) - self.logger.Indent() - lab_query = model.LabModel.query(model.LabModel.name == target_lab) - target_labs = lab_query.fetch() - - available_devices = {} - if target_labs: - for lab in target_labs: - self.logger.Println("- Host: %s" % lab.hostname) - self.logger.Indent() - device_query = model.DeviceModel.query( - model.DeviceModel.hostname == lab.hostname) - host_devices = device_query.fetch() - - for device in host_devices: - if ((device.status in [ - Status.DEVICE_STATUS_DICT["fastboot"], - Status.DEVICE_STATUS_DICT["online"], - Status.DEVICE_STATUS_DICT["ready"] - ]) and (device.scheduling_status == - Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) - and device.product.lower() == target_product_type.lower()): - self.logger.Println( - "- Found %s %s %s" % (device.product, - device.status, - device.serial)) - if device.hostname not in available_devices: - available_devices[device.hostname] = set() - available_devices[device.hostname].add( - device.serial) - self.logger.Unindent() - for host in available_devices: - if len(available_devices[host]) >= schedule.shards: - self.logger.Println("All devices found.") - self.logger.Unindent() - return host, target_device, list( - available_devices[host])[:schedule.shards] - self.logger.Println( - "- Not enough devices found, while %s required.\n%s" % ( - schedule.shards, available_devices)) - self.logger.Unindent() - self.logger.Unindent() - return None, None, [] + "
\nScheduling task is enqueued. ETA {}\n
".format( + task.eta)) diff --git a/gae/webapp/src/scheduler/periodic_test.py b/gae/webapp/src/scheduler/periodic_test.py deleted file mode 100644 index 5839d6e..0000000 --- a/gae/webapp/src/scheduler/periodic_test.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2018 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import unittest - -try: - from unittest import mock -except ImportError: - import mock - -from webapp.src import vtslab_status as Status -from webapp.src.proto import model -from webapp.src.scheduler import periodic - -from google.appengine.ext import ndb -from google.appengine.ext import testbed - - -class PeriodicSchedulerTest(unittest.TestCase): - """Tests for PeriodicScheduler. - - Attributes: - testbed: A Testbed instance which provides local unit testing. - scheduler: A mock periodic.PeriodicScheduler. - """ - - def setUp(self): - """Initializes test""" - # Create the Testbed class instance and initialize service stubs. - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - # Clear cache between tests. - ndb.get_context().clear_cache() - # Mocking PeriodicScheduler and essential methods. - self.scheduler = periodic.PeriodicScheduler(mock.Mock()) - self.scheduler.response = mock.Mock() - self.scheduler.response.write = mock.Mock() - - def tearDown(self): - self.testbed.deactivate() - - def testSimpleJobCreation(self): - """Asserts a job is created. - - This test defines that each model only has a single entity, and asserts - that a job is created. - """ - schedule = model.ScheduleModel() - schedule.schedule_type = "test" - schedule.test_name = "vts/vts" - schedule.manifest_branch = "branch1" - schedule.build_target = "product1-type1" - schedule.device = ["test_lab1/product1"] - schedule.shards = 1 - schedule.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - schedule.require_signed_device_build = False - schedule.put() - - lab = model.LabModel() - lab.name = "test_lab1" - lab.hostname = "test_lab1_host1" - lab.put() - - device = model.DeviceModel() - device.status = Status.DEVICE_STATUS_DICT["fastboot"] - device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT["free"] - device.product = "product1" - device.serial = "serial1" - device.hostname = "test_lab1_host1" - device.put() - - build = model.BuildModel() - build.manifest_branch = "branch1" - build.build_id = "0000000" - build.build_target = "product1" - build.build_type = "type1" - build.put() - - self.scheduler.get() - self.assertEqual(1, len(model.JobModel.query().fetch())) - print("A job is created successfully.") - - device_query = model.DeviceModel.query( - model.DeviceModel.serial == "serial1") - device = device_query.fetch()[0] - self.assertEqual(Status.DEVICE_SCHEDULING_STATUS_DICT["reserved"], - device.scheduling_status) - print("A device is reserved successfully.") - - -if __name__ == "__main__": - unittest.main() diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py new file mode 100644 index 0000000..f9091ee --- /dev/null +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import datetime +import logging + +from webapp.src import vtslab_status as Status +from webapp.src.proto import model +import webapp2 + + +class ScheduleHandler(webapp2.RequestHandler): + """Background worker class for /worker/schedule_handler. + + This class pull tasks from 'queue-schedule' queue and processes in + background service 'worker'. + """ + + def ReserveDevices(self, target_device_serials): + """Reserves devices. + + Args: + target_device_serials: a list of strings, containing target device + serial numbers. + """ + device_query = model.DeviceModel.query( + model.DeviceModel.serial.IN(target_device_serials)) + devices = device_query.fetch() + for device in devices: + device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ + "reserved"] + device.put() + + def FindBuildId(self, new_job): + """Finds build ID for a new job. + + Args: + new_job: JobModel, a new job. + + Return: + string, build ID found. + """ + build_id = "" + build_query = model.BuildModel.query( + model.BuildModel.manifest_branch == new_job.manifest_branch) + builds = build_query.fetch() + + if builds: + logging.info("-- Find build ID") + # Remove builds if build_id info is none + build_id_filled = [x for x in builds if x.build_id] + sorted_list = sorted( + build_id_filled, key=lambda x: int(x.build_id), reverse=True) + filtered_list = [ + x for x in sorted_list + if (all( + hasattr(x, attrs) + for attrs in ["build_target", "build_type", "build_id"]) + and x.build_target and x.build_type) + ] + for device_build in filtered_list: + candidate_build_target = "-".join( + [device_build.build_target, device_build.build_type]) + if (new_job.build_target == candidate_build_target + and (not new_job.require_signed_device_build + or device_build.signed)): + build_id = device_build.build_id + break + return build_id + + def post(self): + schedule_query = model.ScheduleModel.query() + schedules = schedule_query.fetch() + + if schedules: + logging.info(len(schedules)) + for schedule in schedules: + logging.info("Schedule: %s (branch: %s)" % + (schedule.test_name, schedule.manifest_branch)) + logging.info("Build Target: %s" % schedule.build_target) + logging.info("Device: %s" % schedule.device) + if not self.NewPeriod(schedule): + logging.info("- Skipped") + continue + + target_host, target_device, target_device_serials = ( + self.SelectTargetLab(schedule)) + if not target_host: + continue + + logging.info("- Target host: %s" % target_host) + logging.info("- Target device: %s" % target_device) + logging.info("- Target serials: %s" % target_device_serials) + # TODO: update device status + + # create job and add. + new_job = model.JobModel() + new_job.hostname = target_host + new_job.priority = schedule.priority + new_job.test_name = schedule.test_name + new_job.require_signed_device_build = ( + schedule.require_signed_device_build) + new_job.device = target_device + new_job.period = schedule.period + new_job.serial.extend(target_device_serials) + new_job.build_storage_type = schedule.build_storage_type + new_job.manifest_branch = schedule.manifest_branch + new_job.build_target = schedule.build_target + new_job.shards = schedule.shards + new_job.param = schedule.param + new_job.retry_count = schedule.retry_count + new_job.gsi_storage_type = schedule.gsi_storage_type + new_job.gsi_branch = schedule.gsi_branch + new_job.gsi_build_target = schedule.gsi_build_target + new_job.gsi_pab_account_id = schedule.gsi_pab_account_id + new_job.gsi_vendor_version = schedule.gsi_vendor_version + new_job.test_storage_type = schedule.test_storage_type + new_job.test_branch = schedule.test_branch + new_job.test_build_target = schedule.test_build_target + new_job.test_pab_account_id = (schedule.test_pab_account_id) + + new_job.build_id = "" + + if new_job.build_storage_type == ( + Status.STORAGE_TYPE_DICT["PAB"]): + new_job.build_id = self.FindBuildId(new_job) + if new_job.build_id: + self.ReserveDevices(target_device_serials) + new_job.status = Status.JOB_STATUS_DICT["ready"] + new_job.timestamp = datetime.datetime.now() + new_job.put() + logging.info("NEW JOB") + # else: + logging.info("NO BUILD FOUND") + elif new_job.build_storage_type == ( + Status.STORAGE_TYPE_DICT["GCS"]): + new_job.status = Status.JOB_STATUS_DICT["ready"] + new_job.timestamp = datetime.datetime.now() + new_job.put() + logging.info("NEW JOB - GCS") + else: + logging.info("Unexpected storage type (%s)." % + new_job.build_storage_type) + + logging.info("scheduling done.") + + def NewPeriod(self, schedule): + """Checks whether a new job creation is needed. + + Args: + schedule: a proto containing schedule information. + + Returns: + True if new job is required, False otherwise. + """ + job_query = model.JobModel.query( + model.JobModel.manifest_branch == schedule.manifest_branch, + model.JobModel.build_target == schedule.build_target, + model.JobModel.test_name == schedule.test_name, + model.JobModel.period == schedule.period, + model.JobModel.shards == schedule.shards, + model.JobModel.retry_count == schedule.retry_count, + model.JobModel.gsi_branch == schedule.gsi_branch, + model.JobModel.test_branch == schedule.test_branch) + same_jobs = job_query.fetch() + same_jobs = [ + x for x in same_jobs + if (set(x.param) == set(schedule.param) + and x.device in schedule.device) + ] + if not same_jobs: + return True + + outdated_jobs = [ + x for x in same_jobs + if (datetime.datetime.now() - x.timestamp > datetime.timedelta( + minutes=x.period)) + ] + outdated_ready_jobs = [ + x for x in outdated_jobs + if x.status == Status.JOB_STATUS_DICT["expired"] + ] + + if outdated_ready_jobs: + logging.info( + "Job key[{}] is(are) outdated. " + "They became infra-err status.").format( + ", ".join([str(x.key.id()) for x in outdated_ready_jobs])) + for job in outdated_ready_jobs: + job.status = Status.JOB_STATUS_DICT["infra-err"] + job.put() + + outdated_leased_jobs = [ + x for x in outdated_jobs + if x.status == Status.JOB_STATUS_DICT["leased"] + ] + if outdated_leased_jobs: + logging.info( + "Job key[{}] is(are) expected to be completed " + "however still in leased status.").format( + ", ".join([str(x.key.id()) for x in outdated_leased_jobs])) + + recent_jobs = [x for x in same_jobs if x not in outdated_jobs] + + if recent_jobs or outdated_leased_jobs: + return False + else: + return True + + def SelectTargetLab(self, schedule): + """Find target host and devices to schedule a new job. + + Args: + schedule: a proto containing the information of a schedule. + + Returns: + a string which represents hostname, + a string containing target lab and product with '/' separator, + a list of selected devices serial (see whether devices will be + selected later when the job is picked up.) + """ + for target_device in schedule.device: + if "/" not in target_device: + # device malformed + continue + + target_lab, target_product_type = target_device.split("/") + logging.info("- Lab %s" % target_lab) + lab_query = model.LabModel.query(model.LabModel.name == target_lab) + target_labs = lab_query.fetch() + + available_devices = {} + if target_labs: + for lab in target_labs: + logging.info("- Host: %s" % lab.hostname) + device_query = model.DeviceModel.query( + model.DeviceModel.hostname == lab.hostname) + host_devices = device_query.fetch() + + for device in host_devices: + if ((device.status in [ + Status.DEVICE_STATUS_DICT["fastboot"], + Status.DEVICE_STATUS_DICT["online"], + Status.DEVICE_STATUS_DICT["ready"] + ]) and (device.scheduling_status == + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) + and device.product.lower() == + target_product_type.lower()): + logging.info("- Found %s %s %s" % + (device.product, device.status, + device.serial)) + if device.hostname not in available_devices: + available_devices[device.hostname] = set() + available_devices[device.hostname].add( + device.serial) + for host in available_devices: + if len(available_devices[host]) >= schedule.shards: + logging.info("All devices found.") + return host, target_device, list( + available_devices[host])[:schedule.shards] + logging.info( + "- Not enough devices found, while %s required.\n%s" % + (schedule.shards, available_devices)) + return None, None, [] diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py new file mode 100644 index 0000000..a350903 --- /dev/null +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.proto import model +from webapp.src.scheduler import schedule_worker + +from google.appengine.ext import ndb +from google.appengine.ext import testbed + + +class ScheduleHandlerTest(unittest.TestCase): + """Tests for ScheduleHandler. + + Attributes: + testbed: A Testbed instance which provides local unit testing. + scheduler: A mock schedule_worker.ScheduleHandler. + """ + + def setUp(self): + """Initializes test""" + # Create the Testbed class instance and initialize service stubs. + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.init_datastore_v3_stub() + self.testbed.init_memcache_stub() + # Clear cache between tests. + ndb.get_context().clear_cache() + # Mocking ScheduleHandler and essential methods. + self.scheduler = schedule_worker.ScheduleHandler(mock.Mock()) + self.scheduler.response = mock.Mock() + self.scheduler.response.write = mock.Mock() + + def tearDown(self): + self.testbed.deactivate() + + def testSimpleJobCreation(self): + """Asserts a job is created. + + This test defines that each model only has a single entity, and asserts + that a job is created. + """ + schedule = model.ScheduleModel() + schedule.schedule_type = "test" + schedule.test_name = "vts/vts" + schedule.manifest_branch = "branch1" + schedule.build_target = "product1-type1" + schedule.device = ["test_lab1/product1"] + schedule.shards = 1 + schedule.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + schedule.require_signed_device_build = False + schedule.put() + + lab = model.LabModel() + lab.name = "test_lab1" + lab.hostname = "test_lab1_host1" + lab.put() + + device = model.DeviceModel() + device.status = Status.DEVICE_STATUS_DICT["fastboot"] + device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT["free"] + device.product = "product1" + device.serial = "serial1" + device.hostname = "test_lab1_host1" + device.put() + + build = model.BuildModel() + build.manifest_branch = "branch1" + build.build_id = "0000000" + build.build_target = "product1" + build.build_type = "type1" + build.put() + + self.scheduler.post() + self.assertEqual(1, len(model.JobModel.query().fetch())) + print("A job is created successfully.") + + device_query = model.DeviceModel.query( + model.DeviceModel.serial == "serial1") + device = device_query.fetch()[0] + self.assertEqual(Status.DEVICE_SCHEDULING_STATUS_DICT["reserved"], + device.scheduling_status) + print("A device is reserved successfully.") + + +if __name__ == "__main__": + unittest.main() diff --git a/gae/webapp/src/worker_main.py b/gae/webapp/src/worker_main.py new file mode 100644 index 0000000..f3ef72c --- /dev/null +++ b/gae/webapp/src/worker_main.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from webapp.src.scheduler import schedule_worker +import webapp2 + +app = webapp2.WSGIApplication([ + ("/worker/schedule_handler", schedule_worker.ScheduleHandler) + ], debug=True) diff --git a/gae/worker.yaml b/gae/worker.yaml new file mode 100644 index 0000000..1d05414 --- /dev/null +++ b/gae/worker.yaml @@ -0,0 +1,9 @@ +runtime: python27 +api_version: 1 +threadsafe: true +service: worker + +handlers: +- url: /.* + script: webapp.src.worker_main.app + login: admin -- cgit v1.2.3 From bca5550f5e6c335f4ab275a4fc75077e2fd40e02 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 10 Apr 2018 14:26:19 +0900 Subject: Add timestamp index Test: mma Bug: 77618305 --- gae/index.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gae/index.yaml b/gae/index.yaml index 6b9e317..1e9e77b 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -8,6 +8,7 @@ indexes: - name: serial - name: status - name: scheduling_status + - name: timestamp - kind: BuildModel ancestor: no @@ -19,6 +20,7 @@ indexes: - name: build_type - name: artifact_type - name: artifacts + - name: timestamp - kind: ScheduleModel ancestor: no @@ -39,6 +41,7 @@ indexes: - name: test_branch - name: test_build_target - name: test_pab_account_id + - name: timestamp - kind: LabModel ancestor: no @@ -49,6 +52,7 @@ indexes: - name: ip - name: script - name: devices + - name: timestamp - kind: JobModel ancestor: no @@ -73,3 +77,4 @@ indexes: - name: test_build_target - name: test_pab_account_id - name: infra_log_url + - name: timestamp -- cgit v1.2.3 From 17a6e3cbd232b0129be9188f5c630175722d99b4 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 11 Apr 2018 16:34:56 +0900 Subject: Enhance indexing performance by applying task queue. Test: URL/tasks/indexing/{build,device,job,lab,schedule} Bug: 77618305 Change-Id: Ibbcd34bbf89f333eca55e4c670c659f2a66b770b --- gae/queue.yaml | 9 ++ gae/webapp/src/tasks/indexing.py | 204 +++++++++++++++++---------------------- gae/webapp/src/webapp_main.py | 7 +- gae/webapp/src/worker_main.py | 5 +- 4 files changed, 100 insertions(+), 125 deletions(-) diff --git a/gae/queue.yaml b/gae/queue.yaml index 87bcebf..b460b11 100644 --- a/gae/queue.yaml +++ b/gae/queue.yaml @@ -6,4 +6,13 @@ queue: max_concurrent_requests: 1 retry_parameters: task_retry_limit: 2 + min_backoff_seconds: 1 + +- name: queue-indexing + mode: push + rate: 1/s + bucket_size: 5 + max_concurrent_requests: 1 + retry_parameters: + task_retry_limit: 7 min_backoff_seconds: 1 \ No newline at end of file diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index 4816363..cf78903 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -15,129 +15,97 @@ # limitations under the License. # -import webapp2 +import logging from webapp.src import vtslab_status as Status from webapp.src.proto import model +import webapp2 +from google.appengine.api import taskqueue +from google.appengine.ext import ndb -class CreateIndex(webapp2.RequestHandler): - """Main class for /tasks/indexing. - - By fetch and put all entities, indexing all existing entities. - """ - - def get(self): - """Fetch and put all entities and display complete message.""" - build_query = model.BuildModel.query() - builds = build_query.fetch() - for build in builds: - build.put() - - schedule_query = model.ScheduleModel.query() - schedules = schedule_query.fetch() - for schedule in schedules: - schedule.put() - - lab_query = model.LabModel.query() - labs = lab_query.fetch() - for lab in labs: - lab.put() - - device_query = model.DeviceModel.query() - devices = device_query.fetch() - for device in devices: - device.put() - - job_query = model.JobModel.query() - jobs = job_query.fetch() - for job in jobs: - job.put() - - self.response.write("
Indexing has been completed.
") - - -class CreateBuildModelIndex(webapp2.RequestHandler): - """Main class for /tasks/indexing/build. - - By fetch and put all entities, indexing all existing BuildModel entities. - """ - - def get(self): - """Fetch and put all BuildModel entities""" - build_query = model.BuildModel.query() - builds = build_query.fetch() - for build in builds: - build.put() - - self.response.write("
BuildModel indexing has been completed.
") - - -class CreateDeviceModelIndex(webapp2.RequestHandler): - """Main class for /tasks/indexing/device. - - By fetch and put all entities, indexing all existing DeviceModel entities. - """ - - def get(self): - """Fetch and put all DeviceModel entities""" - device_query = model.DeviceModel.query() - devices = device_query.fetch() - for device in devices: - device.put() - - self.response.write( - "
DeviceModel indexing has been completed.
") - - -class CreateJobModelIndex(webapp2.RequestHandler): - """Main class for /tasks/indexing/job. - - By fetch and put all entities, indexing all existing JobModel entities. - """ - - def get(self): - """Fetch and put all JobModel entities""" - job_query = model.JobModel.query() - jobs = job_query.fetch() - for job in jobs: - job.put() - - self.response.write( - "
JobModel indexing has been completed.
") - - -class CreateLabModelIndex(webapp2.RequestHandler): - """Main class for /tasks/indexing/lab. - - By fetch and put all entities, indexing all existing LabModel entities. - """ - - def get(self): - """Fetch and put all LabModel entities""" - lab_query = model.LabModel.query() - labs = lab_query.fetch() - for lab in labs: - lab.put() - - self.response.write( - "
LabModel indexing has been completed.
") - - -class CreateScheduleModelIndex(webapp2.RequestHandler): - """Main class for /tasks/indexing/schedule. - - By fetch and put all entities, indexing all existing ScheduleModel entities. - """ +PAGING_SIZE = 1000 +DICT_MODELS = { + "build": model.BuildModel, + "device": model.DeviceModel, + "lab": model.LabModel, + "job": model.JobModel, + "schedule": model.ScheduleModel +} - def get(self): - """Fetch and put all ScheduleModel entities""" - schedule_query = model.ScheduleModel.query() - schedules = schedule_query.fetch() - for schedule in schedules: - if schedule.build_storage_type is None: - schedule.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - schedule.put() +class CreateIndex(webapp2.RequestHandler): + """Cron class for /tasks/indexing/{model}.""" + def get(self, arg): + """Creates a task to re-index, with given URL format.""" + index_list = [] + if arg: + if arg.startswith("/") and arg[1:].lower() in DICT_MODELS.keys(): + index_list.append(arg[1:].lower()) + else: + self.response.write("
Access Denied. Please visit "
+                                    "/tasks/indexing/{model}
") + return + else: + # accessed by /tasks/indexing + index_list.extend(DICT_MODELS.keys()) self.response.write( - "
ScheduleModel indexing has been completed.
") + "
Re-indexing task{} for {} {} going to be created.
". + format("s" + if len(index_list) > 1 else "", ", ".join(index_list), "are" + if len(index_list) > 1 else "is")) + + for model_type in index_list: + task = taskqueue.add( + url="/worker/indexing", + target="worker", + queue_name="queue-indexing", + transactional=False, + params={ + "model_type": model_type + }) + self.response.write( + "
Re-indexing task for {} is created. ETA: {}
". + format(model_type, task.eta)) + + +class IndexingHandler(webapp2.RequestHandler): + """Task queue handler class to re-index ndb model.""" + def post(self): + """Fetch entities and process model specific jobs.""" + reload(model) + model_type = self.request.get("model_type") + + num_updated = 0 + next_cursor = None + more = True + + while more: + query = DICT_MODELS[model_type].query() + entities, next_cursor, more = query.fetch_page( + PAGING_SIZE, start_cursor=next_cursor) + + to_put = [] + for entity in entities: + if model_type == "build": + pass + elif model_type == "device": + pass + elif model_type == "lab": + pass + elif model_type == "job": + pass + elif model_type == "schedule": + if entity.build_storage_type is None: + entity.build_storage_type = Status.STORAGE_TYPE_DICT[ + "PAB"] + else: + pass + to_put.append(entity) + + if to_put: + ndb.put_multi(to_put) + num_updated += len(to_put) + + logging.info("{} indexing complete with {} updates!".format( + model_type, num_updated)) diff --git a/gae/webapp/src/webapp_main.py b/gae/webapp/src/webapp_main.py index 11c3142..4a1635b 100644 --- a/gae/webapp/src/webapp_main.py +++ b/gae/webapp/src/webapp_main.py @@ -57,12 +57,7 @@ app = webapp2.WSGIApplication( ("/tasks/schedule", periodic.PeriodicScheduler), ("/tasks/device_heartbeat", device_heartbeat.PeriodicDeviceHeartBeat), ("/tasks/job_heartbeat", job_heartbeat.PeriodicJobHeartBeat), - ("/tasks/indexing", indexing.CreateIndex), - ("/tasks/indexing/build", indexing.CreateBuildModelIndex), - ("/tasks/indexing/device", indexing.CreateDeviceModelIndex), - ("/tasks/indexing/job", indexing.CreateJobModelIndex), - ("/tasks/indexing/lab", indexing.CreateLabModelIndex), - ("/tasks/indexing/schedule", indexing.CreateScheduleModelIndex) + ("/tasks/indexing([/]?.*)", indexing.CreateIndex), ], config=config, debug=False) diff --git a/gae/webapp/src/worker_main.py b/gae/webapp/src/worker_main.py index f3ef72c..40afdf9 100644 --- a/gae/webapp/src/worker_main.py +++ b/gae/webapp/src/worker_main.py @@ -15,9 +15,12 @@ # limitations under the License. # +from webapp.src.tasks import indexing from webapp.src.scheduler import schedule_worker import webapp2 + app = webapp2.WSGIApplication([ - ("/worker/schedule_handler", schedule_worker.ScheduleHandler) + ("/worker/schedule_handler", schedule_worker.ScheduleHandler), + ("/worker/indexing", indexing.IndexingHandler) ], debug=True) -- cgit v1.2.3 From 2354c91cc65b1e6010e810d7d1397a011a620c9c Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 11 Apr 2018 19:04:28 +0900 Subject: Improve logging in ScheduleHandler Test: vtslab-schedule-dev.appspot.com Bug: 77618305 Change-Id: Ie148921a81c173b6e7b9d87250f59e7dc836a9f9 --- gae/webapp/src/scheduler/schedule_worker.py | 100 +++++++++++++++++++--------- gae/webapp/src/utils/logger.py | 4 +- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index f9091ee..0a680a3 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -20,15 +20,22 @@ import logging from webapp.src import vtslab_status as Status from webapp.src.proto import model +from webapp.src.utils import logger import webapp2 +MAX_LOG_CHARACTERS = 10000 # maximum number of characters per each log + class ScheduleHandler(webapp2.RequestHandler): """Background worker class for /worker/schedule_handler. This class pull tasks from 'queue-schedule' queue and processes in background service 'worker'. + + Attributes: + logger: Logger class """ + logger = logger.Logger() def ReserveDevices(self, target_device_serials): """Reserves devices. @@ -60,7 +67,7 @@ class ScheduleHandler(webapp2.RequestHandler): builds = build_query.fetch() if builds: - logging.info("-- Find build ID") + self.logger.Println("-- Find build ID") # Remove builds if build_id info is none build_id_filled = [x for x in builds if x.build_id] sorted_list = sorted( @@ -83,28 +90,34 @@ class ScheduleHandler(webapp2.RequestHandler): return build_id def post(self): + self.logger.Clear() schedule_query = model.ScheduleModel.query() schedules = schedule_query.fetch() if schedules: - logging.info(len(schedules)) for schedule in schedules: - logging.info("Schedule: %s (branch: %s)" % - (schedule.test_name, schedule.manifest_branch)) - logging.info("Build Target: %s" % schedule.build_target) - logging.info("Device: %s" % schedule.device) + self.logger.Println("") + self.logger.Println("Schedule: %s (branch: %s)" % + (schedule.test_name, + schedule.manifest_branch)) + self.logger.Println("Build Target: %s" % schedule.build_target) + self.logger.Println("Device: %s" % schedule.device) + self.logger.Indent() if not self.NewPeriod(schedule): - logging.info("- Skipped") + self.logger.Println("- Skipped") + self.logger.Unindent() continue target_host, target_device, target_device_serials = ( self.SelectTargetLab(schedule)) if not target_host: + self.logger.Unindent() continue - logging.info("- Target host: %s" % target_host) - logging.info("- Target device: %s" % target_device) - logging.info("- Target serials: %s" % target_device_serials) + self.logger.Println("- Target host: %s" % target_host) + self.logger.Println("- Target device: %s" % target_device) + self.logger.Println( + "- Target serials: %s" % target_device_serials) # TODO: update device status # create job and add. @@ -143,20 +156,34 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.status = Status.JOB_STATUS_DICT["ready"] new_job.timestamp = datetime.datetime.now() new_job.put() - logging.info("NEW JOB") - # else: - logging.info("NO BUILD FOUND") + self.logger.Println("NEW JOB") + else: + self.logger.Println("NO BUILD FOUND") elif new_job.build_storage_type == ( Status.STORAGE_TYPE_DICT["GCS"]): new_job.status = Status.JOB_STATUS_DICT["ready"] new_job.timestamp = datetime.datetime.now() new_job.put() - logging.info("NEW JOB - GCS") + self.logger.Println("NEW JOB - GCS") else: - logging.info("Unexpected storage type (%s)." % - new_job.build_storage_type) - - logging.info("scheduling done.") + self.logger.Println("Unexpected storage type (%s)." % + new_job.build_storage_type) + self.logger.Unindent() + + self.logger.Println("Scheduling completed.") + + lines = self.logger.Get() + lines = [line.strip() for line in lines] + outputs = [] + chars = 0 + for line in lines: + chars += len(line) + if chars > MAX_LOG_CHARACTERS: + logging.info("\n".join(outputs)) + outputs = [] + chars = len(line) + outputs.append(line) + logging.info("\n".join(outputs)) def NewPeriod(self, schedule): """Checks whether a new job creation is needed. @@ -196,10 +223,11 @@ class ScheduleHandler(webapp2.RequestHandler): ] if outdated_ready_jobs: - logging.info( - "Job key[{}] is(are) outdated. " - "They became infra-err status.").format( - ", ".join([str(x.key.id()) for x in outdated_ready_jobs])) + self.logger.Println( + ("Job key[{}] is(are) outdated. " + "They became infra-err status.").format( + ", ".join( + [str(x.key.id()) for x in outdated_ready_jobs]))) for job in outdated_ready_jobs: job.status = Status.JOB_STATUS_DICT["infra-err"] job.put() @@ -209,10 +237,11 @@ class ScheduleHandler(webapp2.RequestHandler): if x.status == Status.JOB_STATUS_DICT["leased"] ] if outdated_leased_jobs: - logging.info( - "Job key[{}] is(are) expected to be completed " - "however still in leased status.").format( - ", ".join([str(x.key.id()) for x in outdated_leased_jobs])) + self.logger.Println( + ("Job key[{}] is(are) expected to be completed " + "however still in leased status.").format( + ", ".join( + [str(x.key.id()) for x in outdated_leased_jobs]))) recent_jobs = [x for x in same_jobs if x not in outdated_jobs] @@ -239,14 +268,16 @@ class ScheduleHandler(webapp2.RequestHandler): continue target_lab, target_product_type = target_device.split("/") - logging.info("- Lab %s" % target_lab) + self.logger.Println("- Lab %s" % target_lab) + self.logger.Indent() lab_query = model.LabModel.query(model.LabModel.name == target_lab) target_labs = lab_query.fetch() available_devices = {} if target_labs: for lab in target_labs: - logging.info("- Host: %s" % lab.hostname) + self.logger.Println("- Host: %s" % lab.hostname) + self.logger.Indent() device_query = model.DeviceModel.query( model.DeviceModel.hostname == lab.hostname) host_devices = device_query.fetch() @@ -260,19 +291,22 @@ class ScheduleHandler(webapp2.RequestHandler): Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) and device.product.lower() == target_product_type.lower()): - logging.info("- Found %s %s %s" % - (device.product, device.status, - device.serial)) + self.logger.Println("- Found %s %s %s" % + (device.product, device.status, + device.serial)) if device.hostname not in available_devices: available_devices[device.hostname] = set() available_devices[device.hostname].add( device.serial) + self.logger.Unindent() for host in available_devices: if len(available_devices[host]) >= schedule.shards: - logging.info("All devices found.") + self.logger.Println("All devices found.") + self.logger.Unindent() return host, target_device, list( available_devices[host])[:schedule.shards] - logging.info( + self.logger.Println( "- Not enough devices found, while %s required.\n%s" % (schedule.shards, available_devices)) + self.logger.Unindent() return None, None, [] diff --git a/gae/webapp/src/utils/logger.py b/gae/webapp/src/utils/logger.py index eef82ae..20c03d2 100644 --- a/gae/webapp/src/utils/logger.py +++ b/gae/webapp/src/utils/logger.py @@ -36,10 +36,12 @@ class Logger(object): def Get(self): """Retruns a list of all log message strings.""" return self.log_message - + def Println(self, msg): """Stores a new string `msg` to the log buffer.""" indent = " " * self.log_indent + if msg and type(msg) is not str: + msg = str(msg) self.log_message.append(indent + msg) def Indent(self): -- cgit v1.2.3 From 6d67232ed62b934b3602b231b21a77eeb3f77e5b Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 12 Apr 2018 14:43:48 +0900 Subject: Assign a value for unfilled field. Test: vtslab-schedule-test.appspot.com Bug: 77934812 Change-Id: I58f98c5449ee8c3ab3e7488b764715a04a3463f9 --- gae/webapp/src/scheduler/schedule_worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 0a680a3..dc23273 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -133,6 +133,7 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.build_storage_type = schedule.build_storage_type new_job.manifest_branch = schedule.manifest_branch new_job.build_target = schedule.build_target + new_job.pab_account_id = schedule.device_pab_account_id new_job.shards = schedule.shards new_job.param = schedule.param new_job.retry_count = schedule.retry_count -- cgit v1.2.3 From efef426f203cd18e31115a93c3b27895f15bdfb6 Mon Sep 17 00:00:00 2001 From: Hyunwoo Ko Date: Thu, 12 Apr 2018 17:43:16 +0900 Subject: Added API "set_version" to set the version values of already existing hosts. Test: > device Bug: 77941959 --- gae/index.yaml | 1 + gae/webapp/src/endpoint/lab_info.py | 23 +++++++++++++++++++++++ gae/webapp/src/proto/model.py | 2 ++ gae/webapp/static/device.html | 3 +++ 4 files changed, 29 insertions(+) diff --git a/gae/index.yaml b/gae/index.yaml index 1e9e77b..b688e5c 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -53,6 +53,7 @@ indexes: - name: script - name: devices - name: timestamp + - name: vtslab_version - kind: JobModel ancestor: no diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 8945db4..97a6875 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -27,6 +27,8 @@ from webapp.src.proto import model SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer( model.LabInfoMessage) +LAB_HOST_INFO_RESOURCE = endpoints.ResourceContainer( + model.LabHostInfoMessage) @endpoints.api(name='lab_info', version='v1') @@ -80,3 +82,24 @@ class LabInfoApi(remote.Service): return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) + + @endpoints.method( + LAB_HOST_INFO_RESOURCE, + model.DefaultResponse, + path="set_version", + http_method="POST", + name="set_version") + def set_version(self, request): + """Sets vtslab version of the host """ + lab_query = model.LabModel.query( + model.LabModel.hostname == request.hostname + ) + labs = lab_query.fetch() + + for lab in labs: + lab.vtslab_version = request.vtslab_version.split(":")[0] + lab.put() + + return model.DefaultResponse( + return_code=model.ReturnCodeMessage.SUCCESS) + diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 6370f10..e9e0cdf 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -139,6 +139,7 @@ class LabModel(ndb.Model): # devices is a comma-separated list of serial=product pairs devices = ndb.StringProperty() timestamp = ndb.DateTimeProperty(auto_now=False) + vtslab_version = ndb.StringProperty() class LabDeviceInfoMessage(messages.Message): @@ -154,6 +155,7 @@ class LabHostInfoMessage(messages.Message): script = messages.StringField(3) device = messages.MessageField( LabDeviceInfoMessage, 4, repeated=True) + vtslab_version = messages.StringField(5) class LabInfoMessage(messages.Message): diff --git a/gae/webapp/static/device.html b/gae/webapp/static/device.html index 23ef8ce..8d44ac5 100644 --- a/gae/webapp/static/device.html +++ b/gae/webapp/static/device.html @@ -109,6 +109,7 @@ Hostname IP Script + Version {% set index = 1 %} {% for lab in labs %} @@ -126,6 +127,8 @@ {{ lab.ip }} {{ lab.script }} + + {{ lab.vtslab_version }} {% endfor %} -- cgit v1.2.3 From c0b0be74c614ecd2ca5735f5c3296657ff6e1a96 Mon Sep 17 00:00:00 2001 From: Hyunwoo Ko Date: Fri, 13 Apr 2018 16:47:14 +0900 Subject: Added "bootup-err" job status. Test: > device --lease=True Bug: 77997275 --- gae/webapp/src/endpoint/job_queue.py | 3 ++- gae/webapp/src/vtslab_status.py | 4 +++- gae/webapp/static/job.html | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index ea18634..a1ae8a5 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -179,7 +179,8 @@ class JobQueueApi(remote.Service): device.scheduling_status = ( Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) device.put() - elif request.status == Status.JOB_STATUS_DICT["infra-err"]: + elif (request.status == Status.JOB_STATUS_DICT["infra-err"] + or request.status == Status.JOB_STATUS_DICT["bootup-err"]): for device in devices: device.scheduling_status = ( Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) diff --git a/gae/webapp/src/vtslab_status.py b/gae/webapp/src/vtslab_status.py index c200ed2..61dd9e1 100644 --- a/gae/webapp/src/vtslab_status.py +++ b/gae/webapp/src/vtslab_status.py @@ -52,7 +52,9 @@ JOB_STATUS_DICT = { # unexpected error during running "infra-err": 3, # never leased within schedule period - "expired": 4 + "expired": 4, + # device boot error after flashing the given img sets + "bootup-err": 5 } JOB_PRIORITY_DICT = { diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index 957fee8..b2ae2e5 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -192,7 +192,9 @@ {{ {0: "ready", 1: "leased", 2: "complete", - 3: "infra-err"}[job.status] | default("status key error") }} + 3: "infra-err", + 4: "expired", + 5: "bootup-err"}[job.status] | default("status key error") }} {% if job.infra_log_url %} (download log) {% endif %} -- cgit v1.2.3 From 48a7dde3f183ae0761e55260ed3747f443841eab Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Mon, 9 Apr 2018 18:29:25 -0700 Subject: extend scripts for public Test: mma Change-Id: Iebe682fd3d9872f8c26aa0907c651ac94162f83f --- gae/script/build.sh | 4 +++- gae/script/deploy-endpoint.sh | 4 +++- gae/script/deploy-webapp.sh | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/gae/script/build.sh b/gae/script/build.sh index d4015b7..6cc6a97 100755 --- a/gae/script/build.sh +++ b/gae/script/build.sh @@ -15,12 +15,14 @@ # limitations under the License. if [ "$#" -ne 1 ]; then - echo "usage: build.sh prod|test" + echo "usage: build.sh prod|test|public" exit 1 fi if [ $1 = "prod" ]; then SERVICE="vtslab-schedule-prod.appspot.com" +elif [ $1 = "public" ]; then + SERVICE="vtslab-schedule.appspot.com" else SERVICE="vtslab-schedule-test.appspot.com" fi diff --git a/gae/script/deploy-endpoint.sh b/gae/script/deploy-endpoint.sh index fb8f4c1..45ce9ac 100755 --- a/gae/script/deploy-endpoint.sh +++ b/gae/script/deploy-endpoint.sh @@ -15,12 +15,14 @@ # limitations under the License. if [ "$#" -ne 1 ]; then - echo "usage: deploy-endpoint.sh prod|test" + echo "usage: deploy-endpoint.sh prod|test|public" exit 1 fi if [ $1 = "prod" ]; then SERVICE="vtslab-schedule-prod.appspot.com" +elif [ $1 = "public" ]; then + SERVICE="vtslab-schedule.appspot.com" else SERVICE="vtslab-schedule-test.appspot.com" fi diff --git a/gae/script/deploy-webapp.sh b/gae/script/deploy-webapp.sh index a8c9da8..7b3856c 100755 --- a/gae/script/deploy-webapp.sh +++ b/gae/script/deploy-webapp.sh @@ -15,7 +15,7 @@ # limitations under the License. if [ "$#" -ne 1 ]; then - echo "usage: deploy-webapp.sh prod|test|local" + echo "usage: deploy-webapp.sh prod|test|public|local" exit 1 fi @@ -23,6 +23,8 @@ if [ $1 = "prod" ]; then SERVICE="vtslab-schedule-prod" elif [ $1 = "test" ]; then SERVICE="vtslab-schedule-test" +elif [ $1 = "public" ]; then + SERVICE="vtslab-schedule" else dev_appserver.py ./ exit 0 @@ -30,6 +32,6 @@ fi echo "Deploying the web app to $SERVICE ..." -gcloud app deploy app.yaml index.yaml cron.yaml --project=$SERVICE +gcloud app deploy app.yaml cron.yaml index.yaml queue.yaml worker.yaml --project=$SERVICE echo "Deployment done!" -- cgit v1.2.3 From f9b4b9b47ec6512cc40b2a2671845646dae8ca79 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 18 Apr 2018 10:39:35 +0900 Subject: Add an admin field in lab config protobuf. Test: mma Bug: 77999079 --- proto/TestLabConfigMessage.proto | 1 + proto/TestLabConfigMessage_pb2.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/proto/TestLabConfigMessage.proto b/proto/TestLabConfigMessage.proto index ed001b6..bbbe833 100644 --- a/proto/TestLabConfigMessage.proto +++ b/proto/TestLabConfigMessage.proto @@ -21,6 +21,7 @@ message LabConfigMessage { // Lab name where format is labtype-location-instance (e.g., vtslab-mtv32-main). optional bytes name = 1; optional bytes owner = 2; + repeated bytes admin = 3; // For the IP address. repeated HostConfigMessage host = 11; diff --git a/proto/TestLabConfigMessage_pb2.py b/proto/TestLabConfigMessage_pb2.py index fe0c475..383f672 100644 --- a/proto/TestLabConfigMessage_pb2.py +++ b/proto/TestLabConfigMessage_pb2.py @@ -19,7 +19,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( name='TestLabConfigMessage.proto', package='android.test.lab', syntax='proto2', - serialized_pb=_b('\n\x1aTestLabConfigMessage.proto\x12\x10\x61ndroid.test.lab\"b\n\x10LabConfigMessage\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\r\n\x05owner\x18\x02 \x01(\x0c\x12\x31\n\x04host\x18\x0b \x03(\x0b\x32#.android.test.lab.HostConfigMessage\"\x8e\x01\n\x11HostConfigMessage\x12\x10\n\x08hostname\x18\x01 \x01(\x0c\x12\n\n\x02ip\x18\x02 \x01(\x0c\x12\x0e\n\x06script\x18\x03 \x01(\x0c\x12\x14\n\x0csetup_script\x18\x04 \x01(\x0c\x12\x35\n\x06\x64\x65vice\x18\x0b \x03(\x0b\x32%.android.test.lab.DeviceConfigMessage\"E\n\x13\x44\x65viceConfigMessage\x12\x0e\n\x06serial\x18\x01 \x01(\x0c\x12\r\n\x05index\x18\x02 \x01(\x05\x12\x0f\n\x07product\x18\x0b \x01(\x0c') + serialized_pb=_b('\n\x1aTestLabConfigMessage.proto\x12\x10\x61ndroid.test.lab\"q\n\x10LabConfigMessage\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\r\n\x05owner\x18\x02 \x01(\x0c\x12\r\n\x05\x61\x64min\x18\x03 \x03(\x0c\x12\x31\n\x04host\x18\x0b \x03(\x0b\x32#.android.test.lab.HostConfigMessage\"\x8e\x01\n\x11HostConfigMessage\x12\x10\n\x08hostname\x18\x01 \x01(\x0c\x12\n\n\x02ip\x18\x02 \x01(\x0c\x12\x0e\n\x06script\x18\x03 \x01(\x0c\x12\x14\n\x0csetup_script\x18\x04 \x01(\x0c\x12\x35\n\x06\x64\x65vice\x18\x0b \x03(\x0b\x32%.android.test.lab.DeviceConfigMessage\"E\n\x13\x44\x65viceConfigMessage\x12\x0e\n\x06serial\x18\x01 \x01(\x0c\x12\r\n\x05index\x18\x02 \x01(\x05\x12\x0f\n\x07product\x18\x0b \x01(\x0c') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -48,7 +48,14 @@ _LABCONFIGMESSAGE = _descriptor.Descriptor( is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( - name='host', full_name='android.test.lab.LabConfigMessage.host', index=2, + name='admin', full_name='android.test.lab.LabConfigMessage.admin', index=2, + number=3, type=12, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='host', full_name='android.test.lab.LabConfigMessage.host', index=3, number=11, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -67,7 +74,7 @@ _LABCONFIGMESSAGE = _descriptor.Descriptor( oneofs=[ ], serialized_start=48, - serialized_end=146, + serialized_end=161, ) @@ -125,8 +132,8 @@ _HOSTCONFIGMESSAGE = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=149, - serialized_end=291, + serialized_start=164, + serialized_end=306, ) @@ -170,8 +177,8 @@ _DEVICECONFIGMESSAGE = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=293, - serialized_end=362, + serialized_start=308, + serialized_end=377, ) _LABCONFIGMESSAGE.fields_by_name['host'].message_type = _HOSTCONFIGMESSAGE -- cgit v1.2.3 From 0fc5151d64d7716df09f75f2c40f8f34355347cf Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 17 Apr 2018 18:13:54 +0900 Subject: Display time in PST timezone. Test: sequentially tested on local, dev server, and staging server. Bug: 73292349 Change-Id: I12117b856573a3c8c689de39c73ef078c949bddc --- gae/requirements.txt | 3 ++- gae/webapp/src/dashboard/build_list.py | 6 +++--- gae/webapp/src/dashboard/device_list.py | 7 ++----- gae/webapp/src/dashboard/job_list.py | 12 +++++++----- gae/webapp/src/dashboard/schedule_list.py | 4 ++-- gae/webapp/src/handlers/base.py | 27 +++++++++++++++++++++++++-- gae/webapp/src/webapp_main.py | 4 ++-- gae/webapp/static/build.html | 18 +++++++++++++++--- gae/webapp/static/device.html | 8 ++++++-- gae/webapp/static/job.html | 10 +++++++++- gae/webapp/static/schedule.html | 4 ++++ 11 files changed, 77 insertions(+), 26 deletions(-) diff --git a/gae/requirements.txt b/gae/requirements.txt index 25f19c2..760533d 100644 --- a/gae/requirements.txt +++ b/gae/requirements.txt @@ -4,5 +4,6 @@ google-api-python-client # google-endpoints==2.4.5 # google-endpoints-api-management==1.3.0 -arrow +pytz stripe + diff --git a/gae/webapp/src/dashboard/build_list.py b/gae/webapp/src/dashboard/build_list.py index b3bb4ac..2932fc0 100644 --- a/gae/webapp/src/dashboard/build_list.py +++ b/gae/webapp/src/dashboard/build_list.py @@ -15,7 +15,7 @@ # limitations under the License. # -from webapp.src.handlers.base import BaseHandler +from webapp.src.handlers import base from webapp.src.proto import model @@ -87,7 +87,7 @@ def ReadBuildInfo(target_branch=""): return test_builds, device_builds, gsi_builds -class BuildPage(BaseHandler): +class BuildPage(base.BaseHandler): """Main class for /build web page.""" def get(self): @@ -121,7 +121,7 @@ class BuildPage(BaseHandler): all_builds[manifest_branch_key]["gsi"] = [] template_values = { - "all_builds": all_builds, + "all_builds": all_builds } self.render(template_values) diff --git a/gae/webapp/src/dashboard/device_list.py b/gae/webapp/src/dashboard/device_list.py index fd0e53c..4b18cf2 100644 --- a/gae/webapp/src/dashboard/device_list.py +++ b/gae/webapp/src/dashboard/device_list.py @@ -15,9 +15,7 @@ # limitations under the License. # -import datetime - -from webapp.src.handlers.base import BaseHandler +from webapp.src.handlers import base from webapp.src.proto import model from webapp.src import vtslab_status @@ -36,7 +34,7 @@ class DeviceStats(object): error_ratio = -1 -class DevicePage(BaseHandler): +class DevicePage(base.BaseHandler): """Main class for /device web page.""" def get(self): @@ -72,7 +70,6 @@ class DevicePage(BaseHandler): stats.error_ratio = count_error * 100 / stats.total template_values = { - "now": datetime.datetime.now(), "devices": devices, "labs": labs, "stats": stats diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index cfb57a1..af2ab00 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -18,7 +18,7 @@ import datetime from webapp.src import vtslab_status -from webapp.src.handlers.base import BaseHandler +from webapp.src.handlers import base from webapp.src.proto import model @@ -44,7 +44,7 @@ class JobStats(object): unknown = 0 -class JobPage(BaseHandler): +class JobPage(base.BaseHandler): """Main class for /job web page.""" def get(self): @@ -94,7 +94,7 @@ class JobPage(BaseHandler): stats.unknown += 1 -class CreateJobTemplatePage(BaseHandler): +class CreateJobTemplatePage(base.BaseHandler): """Main class for /create_job_template web page.""" def get(self): @@ -104,7 +104,7 @@ class CreateJobTemplatePage(BaseHandler): self.render(template_values) -class CreateJobPage(BaseHandler): +class CreateJobPage(base.BaseHandler): """Main class for /create_job web page.""" def get(self): @@ -183,6 +183,8 @@ class CreateJobPage(BaseHandler): job_query = model.JobModel.query() jobs = job_query.fetch() - template_values = {"message": message, } + template_values = { + "message": message + } self.render(template_values) diff --git a/gae/webapp/src/dashboard/schedule_list.py b/gae/webapp/src/dashboard/schedule_list.py index 627ca3c..fdc1d80 100644 --- a/gae/webapp/src/dashboard/schedule_list.py +++ b/gae/webapp/src/dashboard/schedule_list.py @@ -15,11 +15,11 @@ # limitations under the License. # -from webapp.src.handlers.base import BaseHandler +from webapp.src.handlers import base from webapp.src.proto import model -class SchedulePage(BaseHandler): +class SchedulePage(base.BaseHandler): """Main class for /schedule web page.""" def get(self): diff --git a/gae/webapp/src/handlers/base.py b/gae/webapp/src/handlers/base.py index 28d5393..bc39879 100644 --- a/gae/webapp/src/handlers/base.py +++ b/gae/webapp/src/handlers/base.py @@ -14,12 +14,13 @@ # limitations under the License. # +import datetime import httplib import logging import os import urlparse -import arrow +import pytz import stripe import webapp2 from google.appengine.api import users @@ -29,6 +30,27 @@ from webapp2_extras import sessions import errors +def GetTimeWithTimezone(dt, timezone="US/Pacific"): + """Converts timezone of datetime.datetime() instance. + + Args: + dt: datetime.datetime() instance. + timezone: a string representing timezone listed in TZ database. + + Returns: + datetime.datetime() instance with the given timezone. + """ + if not dt: + return None + utc_time = dt.replace(tzinfo=pytz.utc) + try: + converted_time = utc_time.astimezone(pytz.timezone(timezone)) + except pytz.UnknownTimeZoneError as e: + logging.exception(e) + converted_time = dt + return converted_time + + class BaseHandler(webapp2.RequestHandler): """BaseHandler for all requests.""" @@ -173,12 +195,13 @@ class BaseHandler(webapp2.RequestHandler): resp.update({ # Defaults go here. - 'now': arrow.utcnow(), + 'now': datetime.datetime.now(), 'dest_url': str(self.request.get('dest_url', '')), 'form_errors': self.session.pop('form_errors', []), 'user': user, 'url': url, 'url_linktext': url_linktext, + "convert_time": GetTimeWithTimezone }) if 'preload' not in resp: diff --git a/gae/webapp/src/webapp_main.py b/gae/webapp/src/webapp_main.py index 4a1635b..380db83 100644 --- a/gae/webapp/src/webapp_main.py +++ b/gae/webapp/src/webapp_main.py @@ -23,14 +23,14 @@ from webapp.src.dashboard import build_list from webapp.src.dashboard import device_list from webapp.src.dashboard import job_list from webapp.src.dashboard import schedule_list -from webapp.src.handlers.base import BaseHandler +from webapp.src.handlers import base from webapp.src.scheduler import device_heartbeat from webapp.src.scheduler import job_heartbeat from webapp.src.scheduler import periodic from webapp.src.tasks import indexing -class MainPage(BaseHandler): +class MainPage(base.BaseHandler): """Main web page request handler.""" def get(self): diff --git a/gae/webapp/static/build.html b/gae/webapp/static/build.html index 160c04e..5358f4b 100644 --- a/gae/webapp/static/build.html +++ b/gae/webapp/static/build.html @@ -95,7 +95,11 @@ {{ build.manifest_branch }} - + {{ build.build_id }} @@ -122,7 +126,11 @@ {{ build.manifest_branch }} - + {{ build.build_id }} {{ build.build_target }} @@ -150,7 +158,11 @@ {{ build.manifest_branch }} - + {{ build.build_id }} diff --git a/gae/webapp/static/device.html b/gae/webapp/static/device.html index 8d44ac5..72f8f6a 100644 --- a/gae/webapp/static/device.html +++ b/gae/webapp/static/device.html @@ -58,7 +58,7 @@

Device List

Utilization: {{ stats.utilization }}%, Error Ratio: {{ stats.error_ratio }}%, Total Device Count: {{ stats.total}} - (Now: {{ now }})
+ (Now: {{ convert_time(now).strftime("%Y-%m-%d %H:%M:%S %Z") }})
{% endfor %}
# @@ -94,7 +94,11 @@ 1: "reserved", 2: "use"}[device.scheduling_status] | default("status key error") }} - {{ device.timestamp }} + {% if device.timestamp %} + {{ convert_time(device.timestamp).strftime("%Y-%m-%d %H:%M:%S") }} + {% else %} + {{ device.timestamp }} + {% endif %}
diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index b2ae2e5..656ba96 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -125,7 +125,7 @@
- (Now: {{ now }})
+ (Now: {{ convert_time(now).strftime("%Y-%m-%d %H:%M:%S %Z") }})
{% endfor %}
# @@ -199,9 +199,17 @@ (download log) {% endif %} + {% if job.timestamp %} + {{ convert_time(job.timestamp).strftime("%Y-%m-%d %H:%M:%S") }} + {% else %} {{ job.timestamp }} + {% endif %} + {% if job.heartbeat_stamp %} + {{ convert_time(job.heartbeat_stamp).strftime("%Y-%m-%d %H:%M:%S") }} + {% else %} {{ job.heartbeat_stamp }} + {% endif %}
diff --git a/gae/webapp/static/schedule.html b/gae/webapp/static/schedule.html index 4fcc6e3..2c0539b 100644 --- a/gae/webapp/static/schedule.html +++ b/gae/webapp/static/schedule.html @@ -108,7 +108,11 @@ {{ schedule.priority }} + {% if schedule.timestamp %} + {{ convert_time(schedule.timestamp).strftime("%Y-%m-%d %H:%M:%S") }} + {% else %} {{ schedule.timestamp }} + {% endif %} {% endfor %} -- cgit v1.2.3 From fb5f1e8d4dab0ae2aed3a2631fb4b018c3f841bf Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 17 Apr 2018 14:48:44 +0900 Subject: Include bootup-err in statistics. Test: vtslab-schedule-test.appspot.com Bug: 77997275 Change-Id: I250f80de0d15323de28a544838bb3ead4cefb60e --- gae/webapp/src/dashboard/job_list.py | 10 ++++++--- gae/webapp/static/job.html | 40 +++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index cfb57a1..3945d14 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -26,19 +26,21 @@ class JobStats(object): """Job stats class. Attributes: + boot_error: int, the number of boot-up error jobs. created: int, the number of created jobs. completed: int, the number of completed jobs. - failed: int, the number of failed jobs. expired: int, the number of expired jobs. + infra_error: int, the number of infra error jobs. running: int, the number of running jobs. ready: int, the number of ready jobs. unknown: int, the number of unknown jobs. """ + boot_error = 0 created = 0 completed = 0 - failed = 0 expired = 0 + infra_error = 0 running = 0 ready = 0 unknown = 0 @@ -87,7 +89,9 @@ class JobPage(BaseHandler): elif job.status == vtslab_status.JOB_STATUS_DICT["ready"]: stats.ready += 1 elif job.status == vtslab_status.JOB_STATUS_DICT["infra-err"]: - stats.failed += 1 + stats.infra_error += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["bootup-err"]: + stats.boot_error += 1 elif job.status == vtslab_status.JOB_STATUS_DICT["expired"]: stats.expired += 1 else: diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index b2ae2e5..e0f779d 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -79,25 +79,39 @@ Created Completed Running/Ready - Failed/Expired + Boot up Error + Infra Error + Expired 24 Hours {{ stats_24hrs.created }} {% if stats_24hrs.created %} - {{ stats_24hrs.completed * 100 / stats_24hrs.created }}% + {{ "%0.2f" % (stats_24hrs.completed * 100 / stats_24hrs.created)|float }}% {% else %} n/a {% endif %} {% if stats_24hrs.created %} - {{ (stats_24hrs.running + stats_24hrs.ready) * 100 / stats_24hrs.created }}% + {{ "%0.2f" % ((stats_24hrs.running + stats_24hrs.ready) * 100 / stats_24hrs.created)|float }}% {% else %} n/a {% endif %} {% if stats_24hrs.created %} - {{ (stats_24hrs.failed + stats_24hrs.expired) * 100 / stats_24hrs.created }}% + {{ "%0.2f" % (stats_24hrs.boot_error * 100 / stats_24hrs.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_24hrs.created %} + {{ "%0.2f" % (stats_24hrs.infra_error * 100 / stats_24hrs.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_24hrs.created %} + {{ "%0.2f" % (stats_24hrs.expired * 100 / stats_24hrs.created)|float }}% {% else %} n/a {% endif %} @@ -106,19 +120,31 @@ {{ stats_all.created }} {% if stats_all.created %} - {{ stats_all.completed * 100 / stats_all.created }}% + {{ "%0.2f" % (stats_all.completed * 100 / stats_all.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_all.created %} + {{ "%0.2f" % ((stats_all.running + stats_all.ready) * 100 / stats_all.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_all.created %} + {{ "%0.2f" % (stats_all.boot_error * 100 / stats_all.created)|float }}% {% else %} n/a {% endif %} {% if stats_all.created %} - {{ (stats_all.running + stats_all.ready) * 100 / stats_all.created }}% + {{ "%0.2f" % (stats_all.infra_error * 100 / stats_all.created)|float }}% {% else %} n/a {% endif %} {% if stats_all.created %} - {{ (stats_all.failed + stats_all.expired) * 100 / stats_all.created }}% + {{ "%0.2f" % (stats_all.expired * 100 / stats_all.created)|float }}% {% else %} n/a {% endif %} -- cgit v1.2.3 From 3d7af489d9c5cd682f93933d5e1e934c9a66473d Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 18 Apr 2018 14:07:35 +0900 Subject: Set lab admin from lab_info/v1/set endpoint request. Test: end-to-end test on local Bug: 77999079 --- gae/index.yaml | 1 + gae/webapp/src/endpoint/lab_info.py | 7 ++++--- gae/webapp/src/proto/model.py | 2 ++ gae/webapp/static/device.html | 3 +++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index b688e5c..4b8e401 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -48,6 +48,7 @@ indexes: properties: - name: name - name: owner + - name: admin - name: hostname - name: ip - name: script diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 97a6875..bbd74fd 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -25,7 +25,7 @@ from webapp.src.endpoint import host_info from webapp.src.proto import model -SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer( +LAB_INFO_RESOURCE = endpoints.ResourceContainer( model.LabInfoMessage) LAB_HOST_INFO_RESOURCE = endpoints.ResourceContainer( model.LabHostInfoMessage) @@ -36,7 +36,7 @@ class LabInfoApi(remote.Service): """Endpoint API for lab_info.""" @endpoints.method( - SCHEDULE_INFO_RESOURCE, + LAB_INFO_RESOURCE, model.DefaultResponse, path="clear", http_method="POST", @@ -51,7 +51,7 @@ class LabInfoApi(remote.Service): return_code=model.ReturnCodeMessage.SUCCESS) @endpoints.method( - SCHEDULE_INFO_RESOURCE, + LAB_INFO_RESOURCE, model.DefaultResponse, path="set", http_method="POST", @@ -62,6 +62,7 @@ class LabInfoApi(remote.Service): lab = model.LabModel() lab.name = request.name lab.owner = request.owner + lab.admin = request.admin lab.hostname = host.hostname lab.ip = host.ip lab.script = host.script diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index e9e0cdf..c2a90b0 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -134,6 +134,7 @@ class LabModel(ndb.Model): """A model for representing an individual lab entry.""" name = ndb.StringProperty() owner = ndb.StringProperty() + admin = ndb.StringProperty(repeated=True) hostname = ndb.StringProperty() ip = ndb.StringProperty() # devices is a comma-separated list of serial=product pairs @@ -162,6 +163,7 @@ class LabInfoMessage(messages.Message): """A message for representing an individual lab entry.""" name = messages.StringField(1) owner = messages.StringField(2) + admin = messages.StringField(4, repeated=True) host = messages.MessageField( LabHostInfoMessage, 3, repeated=True) diff --git a/gae/webapp/static/device.html b/gae/webapp/static/device.html index 72f8f6a..2a8db2f 100644 --- a/gae/webapp/static/device.html +++ b/gae/webapp/static/device.html @@ -110,6 +110,7 @@ # Name Owner + Admin Hostname IP Script @@ -125,6 +126,8 @@ {{ lab.name }} {{ lab.owner }} + + {{ lab.admin }} {{ lab.hostname }} -- cgit v1.2.3 From dbbc1757e7e90496e00ad8433d408be2878f3a7e Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 18 Apr 2018 18:13:42 +0900 Subject: Send notification when devices becomes no-response status. Test: vtslab-schedule-dev.appspot.com Bug: 77999079 Change-Id: Ibf61d8b9de39f382372c8c2cfd39e377c7d195b9 --- gae/webapp/src/scheduler/device_heartbeat.py | 37 +++++++++- gae/webapp/src/utils/email_util.py | 106 +++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 gae/webapp/src/utils/email_util.py diff --git a/gae/webapp/src/scheduler/device_heartbeat.py b/gae/webapp/src/scheduler/device_heartbeat.py index 8b77a59..730fc5f 100644 --- a/gae/webapp/src/scheduler/device_heartbeat.py +++ b/gae/webapp/src/scheduler/device_heartbeat.py @@ -16,10 +16,14 @@ # import datetime +import logging import webapp2 +from google.appengine.ext import ndb + from webapp.src import vtslab_status as Status from webapp.src.proto import model +from webapp.src.utils import email_util from webapp.src.utils import logger DEVICE_RESPONSE_TIMEOUT_SECONDS = 300 @@ -48,11 +52,42 @@ class PeriodicDeviceHeartBeat(webapp2.RequestHandler): if (datetime.datetime.now() - x.timestamp ).seconds >= DEVICE_RESPONSE_TIMEOUT_SECONDS ] + devices_to_put = [] + labs_to_alert = {} for device in lost_devices: self.logger.Println("Device[{}] is not responding.".format( device.serial)) device.status = Status.DEVICE_STATUS_DICT["no-response"] - device.put() + devices_to_put.append(device) + + # sending notification + lab_query = model.LabModel.query( + model.LabModel.hostname == device.hostname) + labs = lab_query.fetch() + if labs: + lab = labs[0] + if lab.name not in labs_to_alert: + labs_to_alert[lab.name] = {} + labs_to_alert[lab.name]["_recipients"] = [] + if device.hostname not in labs_to_alert[lab.name]: + labs_to_alert[lab.name][device.hostname] = [] + if lab.owner not in labs_to_alert[lab.name]["_recipients"]: + labs_to_alert[lab.name]["_recipients"].append(lab.owner) + labs_to_alert[lab.name]["_recipients"].extend([ + x for x in lab.admin + if x not in labs_to_alert[lab.name]["_recipients"] + ]) + labs_to_alert[lab.name][device.hostname].append(device.serial) + else: + logging.warning( + "Could not find a lab model for hostname {}".format( + device.hostname)) + continue + + if devices_to_put: + ndb.put_multi(devices_to_put) + if labs_to_alert: + email_util.send_device_notification(labs_to_alert) self.response.write( "
\n" + "\n".join(self.logger.Get()) + "\n
") diff --git a/gae/webapp/src/utils/email_util.py b/gae/webapp/src/utils/email_util.py new file mode 100644 index 0000000..965ab3b --- /dev/null +++ b/gae/webapp/src/utils/email_util.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import re + +from google.appengine.api import app_identity +from google.appengine.api import mail + +SENDER_ADDRESS = "noreply@{}.appspotmail.com".format( + app_identity.get_application_id()) + +SEND_DEVICE_NOTIFICATION_HEADER = "Devices in lab {} are not responding." +SEND_DEVICE_NOTIFICATION_FOOTER = ("You are receiving this email because " + "you are listed as an owner, or an " + "administrator, to the lab {}.\n" + "If you received this email by mistake, " + "please send an email to " + "vtslab-dev@google.com. Thank you.") + + +def send_device_notification(devices): + """Sends notification for not responding devices. + + Args: + devices: a dict containing lab and host information of no-response + devices. + """ + for lab in devices: + email_message = mail.EmailMessage() + email_message.sender = SENDER_ADDRESS + try: + email_message.to = verify_recipient_address( + devices[lab]["_recipients"]) + except ValueError as e: + logging.error(e) + continue + email_message.subject = ( + "[VTS lab] Devices not responding in lab {}".format(lab)) + message = "" + message += SEND_DEVICE_NOTIFICATION_HEADER.format(lab) + message += "\n\n" + for host in devices[lab]: + if host == "_recipients" or not devices[lab][host]: + continue + message += "hostname\n" + message += host + message += "\n\ndevices\n" + message += "\n".join(devices[lab][host]) + message += "\n\n\n" + message += "\n\n" + message += SEND_DEVICE_NOTIFICATION_FOOTER.format(lab) + + try: + email_message.body = message + email_message.check_initialized() + email_message.send() + except mail.MissingRecipientError as e: + logging.exception(e) + + +def verify_recipient_address(address): + """Verifies recipients address. + + Args: + address: a list of strings or a string, recipient(s) address. + + Returns: + A list of verified addresses if list type argument is given, or + a string of a verified address if str type argument is given. + + Raises: + ValueError if type of address is neither list nor str. + """ + # pattern for 'any@google.com', and 'any name ' + verify_patterns = [ + re.compile(".*@google\.com$"), + re.compile(".*<.*@google\.com>$") + ] + if not address: + return None + if type(address) is list: + verified_address = [ + x for x in address + if any(pattern.match(x) for pattern in verify_patterns) + ] + return verified_address + elif type(address) is str: + return address if any( + pattern.match(address) for pattern in verify_patterns) else None + else: + raise ValueError("Wrong type - {}.".format(type(address))) -- cgit v1.2.3 From f1dcfae205f348c2d11b7c4bc9953ca82c4f3a90 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 19 Apr 2018 14:08:27 +0900 Subject: Send notification when job error is occurred. Test: vtslab-schedule-dev.appspot.com Bug: 77999079 Change-Id: Idc21737eea8c0b2b360fea6466fc507715e7f605 --- gae/webapp/src/endpoint/job_queue.py | 12 ++- gae/webapp/src/scheduler/job_heartbeat.py | 10 ++- gae/webapp/src/utils/email_util.py | 118 +++++++++++++++++++++++++++--- 3 files changed, 126 insertions(+), 14 deletions(-) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index a1ae8a5..43ce276 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -20,8 +20,9 @@ import re from protorpc import remote -from webapp.src.proto import model from webapp.src import vtslab_status as Status +from webapp.src.proto import model +from webapp.src.utils import email_util JOB_QUEUE_RESOURCE = endpoints.ResourceContainer(model.JobMessage) GCS_URL_PREFIX = "gs://" @@ -175,18 +176,22 @@ class JobQueueApi(remote.Service): logging.debug("[heartbeat] - devices = {}".format( ", ".join([device.serial for device in devices]))) if request.status == Status.JOB_STATUS_DICT["complete"]: + job.status = request.status for device in devices: device.scheduling_status = ( Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) device.put() - elif (request.status == Status.JOB_STATUS_DICT["infra-err"] - or request.status == Status.JOB_STATUS_DICT["bootup-err"]): + elif (request.status in [Status.JOB_STATUS_DICT["infra-err"], + Status.JOB_STATUS_DICT["bootup-err"]]): + job.status = request.status + email_util.send_job_notification(job) for device in devices: device.scheduling_status = ( Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) device.status = Status.DEVICE_STATUS_DICT["unknown"] device.put() elif request.status == Status.JOB_STATUS_DICT["leased"]: + job.status = request.status for device in devices: device.timestamp = datetime.datetime.now() device.put() @@ -205,7 +210,6 @@ class JobQueueApi(remote.Service): job.infra_log_url = request.infra_log_url else: logging.debug("[heartbeat] Wrong infra_log_url address.") - job.status = request.status job.heartbeat_stamp = datetime.datetime.now() job.put() return model.JobLeaseResponse( diff --git a/gae/webapp/src/scheduler/job_heartbeat.py b/gae/webapp/src/scheduler/job_heartbeat.py index 52dc712..836eb1d 100644 --- a/gae/webapp/src/scheduler/job_heartbeat.py +++ b/gae/webapp/src/scheduler/job_heartbeat.py @@ -18,8 +18,11 @@ import datetime import webapp2 +from google.appengine.ext import ndb + from webapp.src import vtslab_status as Status from webapp.src.proto import model +from webapp.src.utils import email_util from webapp.src.utils import logger JOB_RESPONSE_TIMEOUT_SECONDS = 300 @@ -54,13 +57,14 @@ class PeriodicJobHeartBeat(webapp2.RequestHandler): job_timestamp).seconds >= JOB_RESPONSE_TIMEOUT_SECONDS: lost_jobs.append(job) + lost_jobs_to_put = [] for job in lost_jobs: self.logger.Println("Lost job found") self.logger.Println( "[hostname]{} [device]{} [test_name]{}".format( job.hostname, job.device, job.test_name)) job.status = Status.JOB_STATUS_DICT["infra-err"] - job.put() + lost_jobs_to_put.append(job) device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial) @@ -71,6 +75,10 @@ class PeriodicJobHeartBeat(webapp2.RequestHandler): device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ "free"] device.put() + if lost_jobs_to_put: + ndb.put_multi(lost_jobs_to_put) + email_util.send_job_notification(lost_jobs_to_put) + self.response.write( "
\n" + "\n".join(self.logger.Get()) + "\n
") diff --git a/gae/webapp/src/utils/email_util.py b/gae/webapp/src/utils/email_util.py index 965ab3b..fd327f0 100644 --- a/gae/webapp/src/utils/email_util.py +++ b/gae/webapp/src/utils/email_util.py @@ -15,22 +15,35 @@ # limitations under the License. # +import datetime import logging import re from google.appengine.api import app_identity from google.appengine.api import mail +from webapp.src import vtslab_status as Status +from webapp.src.handlers import base +from webapp.src.proto import model + SENDER_ADDRESS = "noreply@{}.appspotmail.com".format( app_identity.get_application_id()) +SEND_NOTIFICATION_FOOTER = ( + "You are receiving this email because you are " + "listed as an owner, or an administrator of the " + "lab {}.\nIf you received this email by mistake, " + "please send an email to VTS Lab infra development " + "team. Thank you.") + +SEND_DEVICE_NOTIFICATION_TITLE = ("[VTS lab] Devices not responding in lab {} " + "({})") SEND_DEVICE_NOTIFICATION_HEADER = "Devices in lab {} are not responding." -SEND_DEVICE_NOTIFICATION_FOOTER = ("You are receiving this email because " - "you are listed as an owner, or an " - "administrator, to the lab {}.\n" - "If you received this email by mistake, " - "please send an email to " - "vtslab-dev@google.com. Thank you.") + +SEND_JOB_NOTIFICATION_TITLE = ("[VTS lab] Job error has been occurred in " + "lab {} ({})") +SEND_JOB_NOTIFICATION_HEADER = ("Jobs in lab {} have been completed " + "unexpectedly.") def send_device_notification(devices): @@ -49,8 +62,10 @@ def send_device_notification(devices): except ValueError as e: logging.error(e) continue - email_message.subject = ( - "[VTS lab] Devices not responding in lab {}".format(lab)) + email_message.subject = SEND_DEVICE_NOTIFICATION_TITLE.format( + lab, + base.GetTimeWithTimezone( + datetime.datetime.now()).strftime("%Y-%m-%d")) message = "" message += SEND_DEVICE_NOTIFICATION_HEADER.format(lab) message += "\n\n" @@ -63,7 +78,92 @@ def send_device_notification(devices): message += "\n".join(devices[lab][host]) message += "\n\n\n" message += "\n\n" - message += SEND_DEVICE_NOTIFICATION_FOOTER.format(lab) + message += SEND_NOTIFICATION_FOOTER.format(lab) + + try: + email_message.body = message + email_message.check_initialized() + email_message.send() + except mail.MissingRecipientError as e: + logging.exception(e) + + +def send_job_notification(jobs): + """Sends notification for job error. + + Args: + jobs: a JobModel entity, or a list of JobModel entities. + """ + if not jobs: + return + if type(jobs) is not list: + jobs = [jobs] + + # grouping jobs by lab to send to each lab owner and admins at once. + labs_to_alert = {} + for job in jobs: + lab_query = model.LabModel.query( + model.LabModel.hostname == job.hostname) + labs = lab_query.fetch() + if labs: + lab = labs[0] + if lab.name not in labs_to_alert: + labs_to_alert[lab.name] = {} + labs_to_alert[lab.name]["jobs"] = [] + labs_to_alert[lab.name]["_recipients"] = [] + if lab.owner not in labs_to_alert[lab.name]["_recipients"]: + labs_to_alert[lab.name]["_recipients"].append(lab.owner) + labs_to_alert[lab.name]["_recipients"].extend([ + x for x in lab.admin + if x not in labs_to_alert[lab.name]["_recipients"] + ]) + labs_to_alert[lab.name]["jobs"].append(job) + else: + logging.warning( + "Could not find a lab model for hostname {}".format( + job.hostname)) + continue + + for lab in labs_to_alert: + email_message = mail.EmailMessage() + email_message.sender = SENDER_ADDRESS + try: + email_message.to = verify_recipient_address( + labs_to_alert[lab]["_recipients"]) + except ValueError as e: + logging.error(e) + continue + email_message.subject = SEND_JOB_NOTIFICATION_TITLE.format( + lab, + base.GetTimeWithTimezone( + datetime.datetime.now()).strftime("%Y-%m-%d")) + message = "" + message += SEND_JOB_NOTIFICATION_HEADER.format(lab) + message += "\n\n" + message += "http://{}.appspot.com/job".format( + app_identity.get_application_id()) + message += "\n\n" + for job in labs_to_alert[lab]["jobs"]: + message += "hostname: {}\n\n".format(job.hostname) + message += "device: {}\n".format(job.device.split("/")[1]) + message += "device serial: {}\n".format(", ".join(job.serial)) + message += ( + "device: branch - {}, target - {}, build_id - {}\n").format( + job.manifest_branch, job.build_target, job.build_id) + message += "gsi: branch - {}, target - {}, build_id - {}\n".format( + job.gsi_branch, job.gsi_build_target, job.gsi_build_id) + message += "test: branch - {}, target - {}, build_id - {}\n".format( + job.test_branch, job.test_build_target, job.test_build_id) + message += "job created: {}\n".format( + base.GetTimeWithTimezone( + job.timestamp).strftime("%Y-%m-%d %H:%M:%S %Z")) + message += "job status: {}\n".format([ + key for key, value in Status.JOB_STATUS_DICT.items() + if value == job.status + ][0]) + message += "\n\n\n" + message += "\n\n" + message += SEND_NOTIFICATION_FOOTER.format(lab) try: email_message.body = message -- cgit v1.2.3 From fc5352d5a82931cb68b4996a1a52d9e56d3fb6c1 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 9 Apr 2018 18:06:13 +0900 Subject: Use ancestor relationship between Schedule and Job. Test: vtslab-schedule-dev.appspot.com Bug: 77618305 Change-Id: I57ad85e3035a35031afd0819392fb247756097c3 --- gae/index.yaml | 2 ++ gae/webapp/src/proto/model.py | 4 ++++ gae/webapp/src/scheduler/schedule_worker.py | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index 4b8e401..93decbc 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -42,6 +42,7 @@ indexes: - name: test_build_target - name: test_pab_account_id - name: timestamp + - name: children_jobs - kind: LabModel ancestor: no @@ -80,3 +81,4 @@ indexes: - name: test_pab_account_id - name: infra_log_url - name: timestamp + - name: parent_schedule diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index c2a90b0..0bef882 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -86,6 +86,8 @@ class ScheduleModel(ndb.Model): timestamp = ndb.DateTimeProperty(auto_now=False) retry_count = ndb.IntegerProperty() + children_jobs = ndb.KeyProperty(kind="JobModel", repeated=True) + class ScheduleControlInfoMessage(messages.Message): """A message for representing a schedule control data entry.""" @@ -235,6 +237,8 @@ class JobModel(ndb.Model): infra_log_url = ndb.StringProperty() + parent_schedule = ndb.KeyProperty(kind="ScheduleModel") + class JobMessage(messages.Message): """A message for representing an individual job entry.""" diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index dc23273..13c6837 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -146,6 +146,7 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.test_branch = schedule.test_branch new_job.test_build_target = schedule.test_build_target new_job.test_pab_account_id = (schedule.test_pab_account_id) + new_job.parent_schedule = schedule.key new_job.build_id = "" @@ -156,7 +157,8 @@ class ScheduleHandler(webapp2.RequestHandler): self.ReserveDevices(target_device_serials) new_job.status = Status.JOB_STATUS_DICT["ready"] new_job.timestamp = datetime.datetime.now() - new_job.put() + new_job_key = new_job.put() + schedule.children_jobs.append(new_job_key) self.logger.Println("NEW JOB") else: self.logger.Println("NO BUILD FOUND") @@ -164,7 +166,8 @@ class ScheduleHandler(webapp2.RequestHandler): Status.STORAGE_TYPE_DICT["GCS"]): new_job.status = Status.JOB_STATUS_DICT["ready"] new_job.timestamp = datetime.datetime.now() - new_job.put() + new_job_key = new_job.put() + schedule.children_jobs.append(new_job_key) self.logger.Println("NEW JOB - GCS") else: self.logger.Println("Unexpected storage type (%s)." % -- cgit v1.2.3 From 88cf0b034090acb9fc86c5e272eb9c42caade006 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 19 Apr 2018 19:15:26 +0900 Subject: Re-index JobModel entities to have parent schedule. Test: ./script/run-unittest.sh Bug: 77618305 Change-Id: Ib89b6b5340fff1613700962b4a8757d288e45224 --- gae/index.yaml | 2 + gae/webapp/src/proto/model.py | 17 +++ gae/webapp/src/tasks/indexing.py | 46 +++++- gae/webapp/src/tasks/indexing_test.py | 271 ++++++++++++++++++++++++++++++++++ 4 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 gae/webapp/src/tasks/indexing_test.py diff --git a/gae/index.yaml b/gae/index.yaml index 4b8e401..93decbc 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -42,6 +42,7 @@ indexes: - name: test_build_target - name: test_pab_account_id - name: timestamp + - name: children_jobs - kind: LabModel ancestor: no @@ -80,3 +81,4 @@ indexes: - name: test_pab_account_id - name: infra_log_url - name: timestamp + - name: parent_schedule diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index c2a90b0..e4ed810 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -86,6 +86,8 @@ class ScheduleModel(ndb.Model): timestamp = ndb.DateTimeProperty(auto_now=False) retry_count = ndb.IntegerProperty() + children_jobs = ndb.KeyProperty(kind="JobModel", repeated=True) + class ScheduleControlInfoMessage(messages.Message): """A message for representing a schedule control data entry.""" @@ -235,6 +237,8 @@ class JobModel(ndb.Model): infra_log_url = ndb.StringProperty() + parent_schedule = ndb.KeyProperty(kind="ScheduleModel") + class JobMessage(messages.Message): """A message for representing an individual job entry.""" @@ -292,3 +296,16 @@ class JobLeaseResponse(messages.Message): """A job lease response proto message.""" return_code = messages.EnumField(ReturnCodeMessage, 1) jobs = messages.MessageField(JobMessage, 2, repeated=True) + + +class KeyValueModel(ndb.Model): + """A simple key-value model. + + This class uses name as key and store one value or more than one values + to store values which require continuous monitoring such as counters, + or flags. + """ + name = ndb.StringProperty() + string_value = ndb.StringProperty() + integer_value = ndb.IntegerProperty() + boolean_value = ndb.BooleanProperty() diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index cf78903..661e062 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -94,7 +94,51 @@ class IndexingHandler(webapp2.RequestHandler): elif model_type == "lab": pass elif model_type == "job": - pass + if not entity.parent_schedule: + # finds and links to a parent schedule. + parent_schedule_query = model.ScheduleModel.query( + model.ScheduleModel.priority == entity.priority, + model.ScheduleModel.test_name == entity.test_name, + model.ScheduleModel.period == entity.period, + model.ScheduleModel.build_storage_type == ( + entity.build_storage_type), + model.ScheduleModel.manifest_branch == ( + entity.manifest_branch), + model.ScheduleModel.build_target == ( + entity.build_target), + model.ScheduleModel.device_pab_account_id == ( + entity.pab_account_id), + model.ScheduleModel.shards == entity.shards, + model.ScheduleModel.retry_count == ( + entity.retry_count), + model.ScheduleModel.gsi_storage_type == ( + entity.gsi_storage_type), + model.ScheduleModel.gsi_branch == ( + entity.gsi_branch), + model.ScheduleModel.gsi_build_target == ( + entity.gsi_build_target), + model.ScheduleModel.gsi_pab_account_id == ( + entity.gsi_pab_account_id), + model.ScheduleModel.gsi_vendor_version == ( + entity.gsi_vendor_version), + model.ScheduleModel.test_storage_type == ( + entity.test_storage_type), + model.ScheduleModel.test_branch == ( + entity.test_branch), + model.ScheduleModel.test_build_target == ( + entity.test_build_target), + model.ScheduleModel.test_pab_account_id == ( + entity.test_pab_account_id) + ) + parent_schedules = parent_schedule_query.fetch() + if not parent_schedules: + logging.error("Parent not found.") + continue + + parent_schedule = parent_schedules[0] + parent_schedule.children_jobs.append(entity.key) + entity.parent_schedule = parent_schedule.key + to_put.append(parent_schedule) elif model_type == "schedule": if entity.build_storage_type is None: entity.build_storage_type = Status.STORAGE_TYPE_DICT[ diff --git a/gae/webapp/src/tasks/indexing_test.py b/gae/webapp/src/tasks/indexing_test.py new file mode 100644 index 0000000..6930b3f --- /dev/null +++ b/gae/webapp/src/tasks/indexing_test.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.proto import model +from webapp.src.tasks import indexing + +from google.appengine.ext import ndb +from google.appengine.ext import testbed + + +class IndexingHandlerTest(unittest.TestCase): + """Tests for IndexingHandler. + + Attributes: + testbed: A Testbed instance which provides local unit testing. + indexing_handler: A mock IndexingHandler instance. + """ + + def setUp(self): + """Initializes test""" + # Create the Testbed class instance and initialize service stubs. + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.init_datastore_v3_stub() + self.testbed.init_memcache_stub() + # Clear cache between tests. + ndb.get_context().clear_cache() + # Mocking IndexingHandler. + self.indexing_handler = indexing.IndexingHandler(mock.Mock()) + self.indexing_handler.request = mock.Mock() + + def tearDown(self): + self.testbed.deactivate() + + def testSingleJobReindexing(self): + """Asserts re-indexing links job and schedule successfully.""" + priority = "top" + test_name = "test/test" + period = 360 + build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + manifest_branch = "manifest_branch" + build_target = "device_build_target-user" + pab_account_id = "1234567890" + shards = 1 + retry_count = 2 + gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + gsi_branch = "gsi_branch" + gsi_build_target = "gsi_build_target-user" + gsi_pab_account_id = "1234567890" + test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + test_branch = "gsi_branch" + test_build_target = "gsi_build_target-user" + test_pab_account_id = "1234567890" + print("\n") + + print("Creating a single schedule...") + schedule = model.ScheduleModel() + schedule.priority = priority + schedule.test_name = test_name + schedule.period = period + schedule.build_storage_type = build_storage_type + schedule.manifest_branch = manifest_branch + schedule.build_target = build_target + schedule.device_pab_account_id = pab_account_id + schedule.shards = shards + schedule.retry_count = retry_count + schedule.gsi_storage_type = gsi_storage_type + schedule.gsi_branch = gsi_branch + schedule.gsi_build_target = gsi_build_target + schedule.gsi_pab_account_id = gsi_pab_account_id + schedule.test_storage_type = test_storage_type + schedule.test_branch = test_branch + schedule.test_build_target = test_build_target + schedule.test_pab_account_id = test_pab_account_id + schedule.put() + + schedules = model.ScheduleModel.query().fetch() + self.assertEqual(1, len(schedules)) + + print("Creating a job for stored schedule...") + for schedule in schedules: + job = model.JobModel() + job.priority = schedule.priority + job.test_name = schedule.test_name + job.period = schedule.period + job.build_storage_type = schedule.build_storage_type + job.manifest_branch = schedule.manifest_branch + job.build_target = schedule.build_target + job.pab_account_id = schedule.device_pab_account_id + job.shards = schedule.shards + job.retry_count = schedule.retry_count + job.gsi_storage_type = schedule.gsi_storage_type + job.gsi_branch = schedule.gsi_branch + job.gsi_build_target = schedule.gsi_build_target + job.gsi_pab_account_id = schedule.gsi_pab_account_id + job.test_storage_type = schedule.test_storage_type + job.test_branch = schedule.test_branch + job.test_build_target = schedule.test_build_target + job.test_pab_account_id = schedule.test_pab_account_id + job.put() + + jobs = model.JobModel.query().fetch() + self.assertEqual(1, len(jobs)) + + print("Seeking children jobs before re-indexing...") + jobs = model.JobModel.query().fetch() + for job in jobs: + parent_key = job.parent_schedule + self.assertIsNone(parent_key) + + print("Seeking children jobs after re-indexing...") + self.indexing_handler.request.get = mock.MagicMock(return_value="job") + self.indexing_handler.post() + jobs = model.JobModel.query().fetch() + for job in jobs: + parent_key = job.parent_schedule + parent_schedule = parent_key.get() + self.assertEqual( + True, + ((parent_schedule.priority == job.priority) and + (parent_schedule.test_name == job.test_name) and + (parent_schedule.period == job.period) and + (parent_schedule.build_storage_type == job.build_storage_type) + and (parent_schedule.manifest_branch == job.manifest_branch) + and (parent_schedule.build_target == job.build_target) and + (parent_schedule.device_pab_account_id == job.pab_account_id) + and (parent_schedule.shards == job.shards) and + (parent_schedule.retry_count == job.retry_count) and + (parent_schedule.gsi_storage_type == job.gsi_storage_type) and + (parent_schedule.gsi_branch == job.gsi_branch) and + (parent_schedule.gsi_build_target == job.gsi_build_target) and + (parent_schedule.gsi_pab_account_id == job.gsi_pab_account_id) + and + (parent_schedule.test_storage_type == job.test_storage_type) + and (parent_schedule.test_branch == job.test_branch) and + (parent_schedule.test_build_target == job.test_build_target) + and (parent_schedule.test_pab_account_id == + job.test_pab_account_id))) + + def testMultiJobReindexing(self): + """Asserts re-indexing links job and schedule successfully.""" + priority = "top" + test_name = "" + period = 360 + build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + manifest_branch = "manifest_branch" + build_target = "device_build_target-user" + pab_account_id = "1234567890" + shards = 1 + retry_count = 2 + gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + gsi_branch = "gsi_branch" + gsi_build_target = "gsi_build_target-user" + gsi_pab_account_id = "1234567890" + test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + test_branch = "gsi_branch" + test_build_target = "gsi_build_target-user" + test_pab_account_id = "1234567890" + print("\n") + + print("Creating four schedules...") + for num in xrange(4): + schedule = model.ScheduleModel() + schedule.priority = priority + schedule.test_name = test_name + str(num + 1) + schedule.period = period + schedule.build_storage_type = build_storage_type + schedule.manifest_branch = manifest_branch + schedule.build_target = build_target + schedule.device_pab_account_id = pab_account_id + schedule.shards = shards + schedule.retry_count = retry_count + schedule.gsi_storage_type = gsi_storage_type + schedule.gsi_branch = gsi_branch + schedule.gsi_build_target = gsi_build_target + schedule.gsi_pab_account_id = gsi_pab_account_id + schedule.test_storage_type = test_storage_type + schedule.test_branch = test_branch + schedule.test_build_target = test_build_target + schedule.test_pab_account_id = test_pab_account_id + schedule.put() + + schedules = model.ScheduleModel.query().fetch() + self.assertEqual(4, len(schedules)) + + print("Creating jobs as number of test_name...") + for schedule in schedules: + for _ in xrange(int(schedule.test_name)): + job = model.JobModel() + job.priority = schedule.priority + job.test_name = schedule.test_name + job.period = schedule.period + job.build_storage_type = schedule.build_storage_type + job.manifest_branch = schedule.manifest_branch + job.build_target = schedule.build_target + job.pab_account_id = schedule.device_pab_account_id + job.shards = schedule.shards + job.retry_count = schedule.retry_count + job.gsi_storage_type = schedule.gsi_storage_type + job.gsi_branch = schedule.gsi_branch + job.gsi_build_target = schedule.gsi_build_target + job.gsi_pab_account_id = schedule.gsi_pab_account_id + job.test_storage_type = schedule.test_storage_type + job.test_branch = schedule.test_branch + job.test_build_target = schedule.test_build_target + job.test_pab_account_id = schedule.test_pab_account_id + job.put() + + jobs = model.JobModel.query().fetch() + self.assertEqual(10, len(jobs)) + + print("Seeking children jobs before re-indexing...") + jobs = model.JobModel.query().fetch() + for job in jobs: + parent_key = job.parent_schedule + self.assertIsNone(parent_key) + + print("Seeking children jobs after re-indexing...") + self.indexing_handler.request.get = mock.MagicMock(return_value="job") + self.indexing_handler.post() + jobs = model.JobModel.query().fetch() + for job in jobs: + parent_key = job.parent_schedule + parent_schedule = parent_key.get() + self.assertEqual( + True, + ((parent_schedule.priority == job.priority) and + (parent_schedule.test_name == job.test_name) and + (parent_schedule.period == job.period) and + (parent_schedule.build_storage_type == job.build_storage_type) + and (parent_schedule.manifest_branch == job.manifest_branch) + and (parent_schedule.build_target == job.build_target) and + (parent_schedule.device_pab_account_id == job.pab_account_id) + and (parent_schedule.shards == job.shards) and + (parent_schedule.retry_count == job.retry_count) and + (parent_schedule.gsi_storage_type == job.gsi_storage_type) and + (parent_schedule.gsi_branch == job.gsi_branch) and + (parent_schedule.gsi_build_target == job.gsi_build_target) and + (parent_schedule.gsi_pab_account_id == job.gsi_pab_account_id) + and + (parent_schedule.test_storage_type == job.test_storage_type) + and (parent_schedule.test_branch == job.test_branch) and + (parent_schedule.test_build_target == job.test_build_target) + and (parent_schedule.test_pab_account_id == + job.test_pab_account_id))) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 4f47ab35a1719458ff70dbb6db352e8625efc539 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 23 Apr 2018 19:41:16 +0900 Subject: Add unit test for job_heartbeat. Test: ./script/run-unittest.sh Bug: 77298544 --- gae/webapp/src/scheduler/job_heartbeat.py | 30 ++-- gae/webapp/src/scheduler/job_heartbeat_test.py | 196 +++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 gae/webapp/src/scheduler/job_heartbeat_test.py diff --git a/gae/webapp/src/scheduler/job_heartbeat.py b/gae/webapp/src/scheduler/job_heartbeat.py index 836eb1d..b7e96fc 100644 --- a/gae/webapp/src/scheduler/job_heartbeat.py +++ b/gae/webapp/src/scheduler/job_heartbeat.py @@ -16,6 +16,7 @@ # import datetime +import logging import webapp2 from google.appengine.ext import ndb @@ -43,8 +44,7 @@ class PeriodicJobHeartBeat(webapp2.RequestHandler): self.logger.Clear() job_query = model.JobModel.query( - model.JobModel.status == Status.JOB_STATUS_DICT["leased"] - ) + model.JobModel.status == Status.JOB_STATUS_DICT["leased"]) jobs = job_query.fetch() lost_jobs = [] @@ -58,27 +58,35 @@ class PeriodicJobHeartBeat(webapp2.RequestHandler): lost_jobs.append(job) lost_jobs_to_put = [] + devices_to_put = [] for job in lost_jobs: self.logger.Println("Lost job found") - self.logger.Println( - "[hostname]{} [device]{} [test_name]{}".format( - job.hostname, job.device, job.test_name)) + self.logger.Println("[hostname]{} [device]{} [test_name]{}".format( + job.hostname, job.device, job.test_name)) job.status = Status.JOB_STATUS_DICT["infra-err"] lost_jobs_to_put.append(job) device_query = model.DeviceModel.query( - model.DeviceModel.serial.IN(job.serial) - ) + model.DeviceModel.serial.IN(job.serial)) devices = device_query.fetch() - for device in devices: + self.logger.Println("Device serial: {}".format(device.serial)) device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ "free"] - device.put() + devices_to_put.append(device) + if lost_jobs_to_put: ndb.put_multi(lost_jobs_to_put) email_util.send_job_notification(lost_jobs_to_put) + self.logger.Println("{} jobs are updated.".format( + len(lost_jobs_to_put))) + + if devices_to_put: + ndb.put_multi(devices_to_put) + self.logger.Println("{} devices are updated.".format( + len(devices_to_put))) + lines = self.logger.Get() + logging.info("\n".join([line.strip() for line in lines])) - self.response.write( - "
\n" + "\n".join(self.logger.Get()) + "\n
") + self.response.write("
\n" + "\n".join(lines) + "\n
") diff --git a/gae/webapp/src/scheduler/job_heartbeat_test.py b/gae/webapp/src/scheduler/job_heartbeat_test.py new file mode 100644 index 0000000..49c8927 --- /dev/null +++ b/gae/webapp/src/scheduler/job_heartbeat_test.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import datetime +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.proto import model +from webapp.src.scheduler import schedule_worker + +from google.appengine.ext import ndb +from google.appengine.ext import testbed + + +class JobHeartbeatTest(unittest.TestCase): + """Tests for PeriodicJobHeartBeat cron class. + + Attributes: + testbed: A Testbed instance which provides local unit testing. + job_heartbeat: A mock job_heartbeat.PeriodicJobHeartBeat instance. + """ + + def setUp(self): + """Initializes test""" + # Create the Testbed class instance and initialize service stubs. + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.setup_env(app_id="vtslab-schedule-unittest") + self.testbed.init_datastore_v3_stub() + self.testbed.init_memcache_stub() + self.testbed.init_mail_stub() + # Clear cache between tests. + ndb.get_context().clear_cache() + # import job_heartbeat after setting app_id. + from webapp.src.scheduler import job_heartbeat + # Mocking PeriodicJobHeartBeat and essential methods. + self.job_heartbeat = job_heartbeat.PeriodicJobHeartBeat(mock.Mock()) + self.job_heartbeat.response = mock.Mock() + self.job_heartbeat.response.write = mock.Mock() + + def tearDown(self): + self.testbed.deactivate() + + def testJobHearbeat(self): + """""" + # schedule information + priority = "top" + period = 360 + build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + manifest_branch = "manifest_branch" + build_target = "device_build_target-user" + pab_account_id = "1234567890" + shards = 2 + retry_count = 1 + gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + gsi_branch = "gsi_branch" + gsi_build_target = "gsi_build_target-user" + gsi_pab_account_id = "1234567890" + test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + test_branch = "gsi_branch" + test_build_target = "gsi_build_target-user" + test_pab_account_id = "1234567890" + + lab_name = "test_lab" + host_name = "test_host" + devices_list = ["device1", "device2"] + + # create a device build + build = model.BuildModel() + build.manifest_branch = manifest_branch + build.build_id = "1000000" + build.build_target = "device_build_target" + build.build_type = "user" + build.artifact_type = "device" + build.timestamp = datetime.datetime.now() + build.signed = False + build.put() + + # create a gsi build + build = model.BuildModel() + build.manifest_branch = gsi_branch + build.build_id = "2000000" + build.build_target = "gsi_build_target" + build.build_type = "user" + build.artifact_type = "gsi" + build.timestamp = datetime.datetime.now() + build.signed = False + build.put() + + # create a test build + build = model.BuildModel() + build.manifest_branch = gsi_branch + build.build_id = "3000000" + build.build_target = "test_build_target" + build.build_type = "user" + build.artifact_type = "test" + build.timestamp = datetime.datetime.now() + build.signed = False + build.put() + + # create a lab + lab = model.LabModel() + lab.name = lab_name + lab.hostname = host_name + lab.owner = "test@google.com" + lab.put() + + # create devices + for dev in devices_list: + for num in xrange(shards): + device = model.DeviceModel() + device.hostname = host_name + device.product = dev + device.serial = "{}{}".format(dev, num) + device.status = Status.DEVICE_STATUS_DICT["fastboot"] + device.scheduling_status = ( + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) + device.timestamp = datetime.datetime.now() + device.put() + + # create schedules + for device in devices_list: + schedule = model.ScheduleModel() + schedule.priority = priority + schedule.test_name = "test/{}".format(device) + schedule.period = period + schedule.build_storage_type = build_storage_type + schedule.manifest_branch = manifest_branch + schedule.build_target = build_target + schedule.device_pab_account_id = pab_account_id + schedule.shards = shards + schedule.retry_count = retry_count + schedule.gsi_storage_type = gsi_storage_type + schedule.gsi_branch = gsi_branch + schedule.gsi_build_target = gsi_build_target + schedule.gsi_pab_account_id = gsi_pab_account_id + schedule.test_storage_type = test_storage_type + schedule.test_branch = test_branch + schedule.test_build_target = test_build_target + schedule.test_pab_account_id = test_pab_account_id + schedule.device = [] + schedule.device.append("{}/{}".format(lab_name, device)) + schedule.put() + + # Mocking ScheduleHandler and essential methods. + scheduler = schedule_worker.ScheduleHandler(mock.Mock()) + scheduler.response = mock.Mock() + scheduler.response.write = mock.Mock() + + print("Creating jobs...") + scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(2, len(jobs)) + + print("Making jobs get old and running heartbeat monitor...") + for job in jobs: + job.status = Status.JOB_STATUS_DICT["leased"] + job.timestamp = ( + datetime.datetime.now() - datetime.timedelta(minutes=10)) + job.heartbeat_stamp = ( + datetime.datetime.now() - datetime.timedelta(minutes=7)) + job.put() + + self.job_heartbeat.get() + + jobs = model.JobModel.query().fetch() + for job in jobs: + self.assertEquals(Status.JOB_STATUS_DICT["infra-err"], job.status) + + devices = model.DeviceModel.query().fetch() + for device in devices: + self.assertEquals(Status.DEVICE_SCHEDULING_STATUS_DICT["free"], + device.scheduling_status) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 346346f23389cda0bafa08e2a4a41d562f272432 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 25 Apr 2018 11:51:41 +0900 Subject: Display job page after manual job creation. Test: dev_appserver.py app.yaml worker.yaml Bug: 78478939 Change-Id: I8209659f22f8bb1862c9dd7c906ead4cb2c9856a --- gae/webapp/src/dashboard/job_list.py | 93 +++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 32 deletions(-) diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index 9bcb212..9e974d9 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -19,6 +19,7 @@ import datetime from webapp.src import vtslab_status from webapp.src.handlers import base +from webapp.src.scheduler import schedule_worker from webapp.src.proto import model @@ -46,7 +47,34 @@ class JobStats(object): unknown = 0 -class JobPage(base.BaseHandler): +class JobBase(base.BaseHandler): + """Base class for job pages.""" + + def _UpdateStats(self, stats, job): + """Updates the stats using the state info of a given job. + + Args: + stats: JobStats, the stats class to update. + job: JobModel, the job to check. + """ + stats.created += 1 + if job.status == vtslab_status.JOB_STATUS_DICT["complete"]: + stats.completed += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["leased"]: + stats.running += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["ready"]: + stats.ready += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["infra-err"]: + stats.infra_error += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["bootup-err"]: + stats.boot_error += 1 + elif job.status == vtslab_status.JOB_STATUS_DICT["expired"]: + stats.expired += 1 + else: + stats.unknown += 1 + + +class JobPage(JobBase): """Main class for /job web page.""" def get(self): @@ -74,29 +102,6 @@ class JobPage(base.BaseHandler): self.render(template_values) - def _UpdateStats(self, stats, job): - """Updates the stats using the state info of a given job. - - Args: - stats: JobStats, the stats class to update. - job: JobModel, the job to check. - """ - stats.created += 1 - if job.status == vtslab_status.JOB_STATUS_DICT["complete"]: - stats.completed += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["leased"]: - stats.running += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["ready"]: - stats.ready += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["infra-err"]: - stats.infra_error += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["bootup-err"]: - stats.boot_error += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["expired"]: - stats.expired += 1 - else: - stats.unknown += 1 - class CreateJobTemplatePage(base.BaseHandler): """Main class for /create_job_template web page.""" @@ -108,7 +113,7 @@ class CreateJobTemplatePage(base.BaseHandler): self.render(template_values) -class CreateJobPage(base.BaseHandler): +class CreateJobPage(JobBase): """Main class for /create_job web page.""" def get(self): @@ -130,13 +135,14 @@ class CreateJobPage(base.BaseHandler): error_devices.append(device.serial) if error_devices: - message = "Can't create a job because at some devices are not available (%s)." % error_devices + message = "Can't create a job because at some devices " \ + "are not available (%s)." % error_devices else: for device in devices: - device.scheduling_status = vtslab_status.DEVICE_SCHEDULING_STATUS_DICT[ - "reserved"] + device.scheduling_status = ( + vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["reserved"]) device.put() - message = "A new job is created! Please click 'Job Queue' menu above to see the new job." + message = "A new job is created!" new_job = model.JobModel() new_job.hostname = self.request.get("hostname", default_value="") @@ -182,13 +188,36 @@ class CreateJobPage(base.BaseHandler): default_value="") new_job.status = vtslab_status.JOB_STATUS_DICT["ready"] new_job.timestamp = datetime.datetime.now() + + test_type = schedule_worker.GetTestVersionType( + new_job.manifest_branch, new_job.gsi_branch) + if new_job.require_signed_device_build: + test_type |= vtslab_status.TEST_TYPE_DICT[ + vtslab_status.TEST_TYPE_SIGNED] + test_type |= ( + vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_MANUAL]) + new_job.test_type = test_type + new_job.put() - job_query = model.JobModel.query() - jobs = job_query.fetch() + job_query = model.JobModel.query() + jobs = job_query.fetch() + + now = datetime.datetime.now() + stats_all = JobStats() + stats_24hrs = JobStats() + if jobs: + for job in jobs: + self._UpdateStats(stats_all, job) + if now - job.timestamp <= datetime.timedelta(hours=24): + self._UpdateStats(stats_24hrs, job) template_values = { - "message": message + "message": message, + "jobs": sorted(jobs, key=lambda x: x.timestamp, + reverse=True), + "stats_all": stats_all, + "stats_24hrs": stats_24hrs } self.render(template_values) -- cgit v1.2.3 From de08af54d3fc6fe3498a9abc15d3e7337e4da2c5 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Fri, 20 Apr 2018 18:32:23 +0900 Subject: Specify job test type. Test: tested on dev schedule server. Bug: 78285062 Change-Id: I527a3de93bd2751589100d9336fbb4d7df7c5cff --- gae/index.yaml | 1 + gae/webapp/src/dashboard/job_list.py | 35 ++++++++++++- gae/webapp/src/endpoint/job_queue.py | 2 + gae/webapp/src/proto/model.py | 5 ++ gae/webapp/src/scheduler/schedule_worker.py | 81 ++++++++++++++++++++++++++++- gae/webapp/src/tasks/indexing.py | 26 ++++++--- gae/webapp/src/vtslab_status.py | 26 +++++++++ gae/webapp/static/job.html | 3 ++ 8 files changed, 169 insertions(+), 10 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index 93decbc..ecb75e8 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -82,3 +82,4 @@ indexes: - name: infra_log_url - name: timestamp - name: parent_schedule + - name: test_type diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index 9e974d9..7eba6ec 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -23,6 +23,35 @@ from webapp.src.scheduler import schedule_worker from webapp.src.proto import model +def test_type_text(test_type, join_str=", "): + """Generates text to represent in HTML with given test type. + + Args: + test_type: an integer, test type value. + join_str: a string, join separator. + + Returns: + A string of test type. + """ + text_list = [] + + if not test_type: + return "Unknown" + + if (test_type & 3) == ( + vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_UNKNOWN]): + return "Unknown" + + if test_type & vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_TOT]: + text_list.append("ToT") + if test_type & vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_OTA]: + text_list.append("OTA") + if test_type & vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_SIGNED]: + text_list.append("Signed") + + return join_str.join(text_list) + + class JobStats(object): """Job stats class. @@ -97,7 +126,8 @@ class JobPage(JobBase): "jobs": sorted(jobs, key=lambda x: x.timestamp, reverse=True), "stats_all": stats_all, - "stats_24hrs": stats_24hrs + "stats_24hrs": stats_24hrs, + "test_type_text": test_type_text } self.render(template_values) @@ -217,7 +247,8 @@ class CreateJobPage(JobBase): "jobs": sorted(jobs, key=lambda x: x.timestamp, reverse=True), "stats_all": stats_all, - "stats_24hrs": stats_24hrs + "stats_24hrs": stats_24hrs, + "test_type_text": test_type_text } self.render(template_values) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 43ce276..28f09f8 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -100,6 +100,7 @@ class JobQueueApi(remote.Service): job_message.test_build_target = job.test_build_target job_message.test_build_id = job.test_build_id job_message.test_pab_account_id = job.test_pab_account_id + job_message.test_type = job.test_type device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) @@ -164,6 +165,7 @@ class JobQueueApi(remote.Service): job_message.status = job.status job_message.period = job.period job_message.retry_count = job.retry_count + job_message.test_type = job.test_type job_messages.append(job_message) device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index e4ed810..526fb3c 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -197,6 +197,8 @@ class HostInfoMessage(messages.Message): class JobModel(ndb.Model): """A model for representing an individual job entry.""" + test_type = ndb.IntegerProperty() + hostname = ndb.StringProperty() priority = ndb.StringProperty() test_name = ndb.StringProperty() @@ -242,6 +244,9 @@ class JobModel(ndb.Model): class JobMessage(messages.Message): """A message for representing an individual job entry.""" + # Next ID = 30 + test_type = messages.IntegerField(29) + hostname = messages.StringField(1) priority = messages.StringField(2) test_name = messages.StringField(3) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 13c6837..818884e 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -17,6 +17,7 @@ import datetime import logging +import re from webapp.src import vtslab_status as Status from webapp.src.proto import model @@ -26,6 +27,76 @@ import webapp2 MAX_LOG_CHARACTERS = 10000 # maximum number of characters per each log +def GetTestVersionType(manifest_branch, gsi_branch, test_type=0): + """Compares manifest branch and gsi branch to get test type. + + This function only completes two LSBs which represent version related + test type. + + Args: + manifest_branch: a string, manifest branch name. + gsi_branch: a string, gsi branch name. + test_type: an integer, previous test type value. + + Returns: + An integer, test type value. + """ + if not test_type: + value = 0 + else: + # clear two bits + value = test_type & ~(1 | 1 << 1) + + if not manifest_branch: + logging.debug("manifest branch cannot be empty or None.") + return value | Status.TEST_TYPE_DICT[Status.TEST_TYPE_UNKNOWN] + + if not gsi_branch: + logging.debug("gsi_branch is empty.") + return value | Status.TEST_TYPE_DICT[Status.TEST_TYPE_TOT] + + gcs_pattern = "^gs://.*/v([0-9.]*)/.*" + p_pattern = "(git_)?p.*" + o_mr1_pattern = "(git_)?o.*mr1.*" + o_pattern = "(git_)?o.*" + master_pattern = "(git_)master" + + gcs_search = re.search(gcs_pattern, manifest_branch) + if gcs_search: + device_version = gcs_search.group(1) + elif re.match(p_pattern, manifest_branch): + device_version = "9.0" + elif re.match(o_mr1_pattern, manifest_branch): + device_version = "8.1" + elif re.match(o_pattern, manifest_branch): + device_version = "8.0" + elif re.match(master_pattern, manifest_branch): + device_version = "master" + else: + logging.debug("Unknown device version.") + return value | Status.TEST_TYPE_DICT[Status.TEST_TYPE_UNKNOWN] + + gcs_search = re.search(gcs_pattern, gsi_branch) + if gcs_search: + gsi_version = gcs_search.group(1) + elif re.match(p_pattern, gsi_branch): + gsi_version = "9.0" + elif re.match(o_mr1_pattern, gsi_branch): + gsi_version = "8.1" + elif re.match(o_pattern, gsi_branch): + gsi_version = "8.0" + elif re.match(master_pattern, gsi_branch): + gsi_version = "master" + else: + logging.debug("Unknown gsi version.") + return value | Status.TEST_TYPE_DICT[Status.TEST_TYPE_UNKNOWN] + + if device_version == gsi_version: + return value | Status.TEST_TYPE_DICT[Status.TEST_TYPE_TOT] + else: + return value | Status.TEST_TYPE_DICT[Status.TEST_TYPE_OTA] + + class ScheduleHandler(webapp2.RequestHandler): """Background worker class for /worker/schedule_handler. @@ -145,9 +216,17 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.test_storage_type = schedule.test_storage_type new_job.test_branch = schedule.test_branch new_job.test_build_target = schedule.test_build_target - new_job.test_pab_account_id = (schedule.test_pab_account_id) + new_job.test_pab_account_id = schedule.test_pab_account_id new_job.parent_schedule = schedule.key + # uses bit 0-1 to indicate version. + test_type = GetTestVersionType(schedule.manifest_branch, + schedule.gsi_branch) + # uses bit 2 + if schedule.require_signed_device_build: + test_type |= Status.TEST_TYPE_DICT[Status.TEST_TYPE_SIGNED] + new_job.test_type = test_type + new_job.build_id = "" if new_job.build_storage_type == ( diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index 661e062..94a8d08 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -19,6 +19,7 @@ import logging from webapp.src import vtslab_status as Status from webapp.src.proto import model +from webapp.src.scheduler import schedule_worker import webapp2 from google.appengine.api import taskqueue @@ -36,6 +37,7 @@ DICT_MODELS = { class CreateIndex(webapp2.RequestHandler): """Cron class for /tasks/indexing/{model}.""" + def get(self, arg): """Creates a task to re-index, with given URL format.""" index_list = [] @@ -71,6 +73,7 @@ class CreateIndex(webapp2.RequestHandler): class IndexingHandler(webapp2.RequestHandler): """Task queue handler class to re-index ndb model.""" + def post(self): """Fetch entities and process model specific jobs.""" reload(model) @@ -94,6 +97,16 @@ class IndexingHandler(webapp2.RequestHandler): elif model_type == "lab": pass elif model_type == "job": + if not entity.test_type: + # uses bits 0-1 to indicate version. + test_type = schedule_worker.GetTestVersionType( + entity.manifest_branch, entity.gsi_branch) + # uses bit 2 + if entity.require_signed_device_build: + test_type |= ( + Status.TEST_TYPE_DICT[Status.TEST_TYPE_SIGNED]) + entity.test_type = test_type + if not entity.parent_schedule: # finds and links to a parent schedule. parent_schedule_query = model.ScheduleModel.query( @@ -128,17 +141,16 @@ class IndexingHandler(webapp2.RequestHandler): model.ScheduleModel.test_build_target == ( entity.test_build_target), model.ScheduleModel.test_pab_account_id == ( - entity.test_pab_account_id) - ) + entity.test_pab_account_id)) parent_schedules = parent_schedule_query.fetch() if not parent_schedules: logging.error("Parent not found.") - continue + else: + parent_schedule = parent_schedules[0] + parent_schedule.children_jobs.append(entity.key) + entity.parent_schedule = parent_schedule.key + to_put.append(parent_schedule) - parent_schedule = parent_schedules[0] - parent_schedule.children_jobs.append(entity.key) - entity.parent_schedule = parent_schedule.key - to_put.append(parent_schedule) elif model_type == "schedule": if entity.build_storage_type is None: entity.build_storage_type = Status.STORAGE_TYPE_DICT[ diff --git a/gae/webapp/src/vtslab_status.py b/gae/webapp/src/vtslab_status.py index 61dd9e1..7ed70ad 100644 --- a/gae/webapp/src/vtslab_status.py +++ b/gae/webapp/src/vtslab_status.py @@ -73,6 +73,32 @@ STORAGE_TYPE_DICT = { } +TEST_TYPE_UNKNOWN = "unknown" +TEST_TYPE_TOT = "ToT" +TEST_TYPE_OTA = "OTA" +TEST_TYPE_SIGNED = "signed" +TEST_TYPE_PRESUBMIT = "presubmit" +TEST_TYPE_MANUAL = "manual" + +# a dict, where keys indicate test type and values have bitwise values. +# bit 0-1 : version related test type +# 00 - Unknown +# 01 - ToT +# 10 - OTA +# bit 2 : device signed build +# bit 3-4 : reserved for gerrit related test type +# 01 - pre-submit +# bit 5 : manually created test job +TEST_TYPE_DICT = { + TEST_TYPE_UNKNOWN: 0, + TEST_TYPE_TOT: 1, + TEST_TYPE_OTA: 1 << 1, + TEST_TYPE_SIGNED: 1 << 2, + TEST_TYPE_PRESUBMIT: 1 << 3, + TEST_TYPE_MANUAL: 1 << 5 +} + + def PrioritySortHelper(priority): """Helper function to sort jobs based on priority. diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index 7fc3322..3e56fb9 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -155,6 +155,7 @@ + {% set index = 1 %} {% for schedule in schedules %} @@ -113,6 +114,12 @@ {% else %} {{ schedule.timestamp }} {% endif %} + {% endfor %}
# + test_type manifest_branch build_target test_name @@ -179,6 +180,8 @@ {{ index }} {% set index = index + 1 %} + + {{ test_type_text(job.test_type) }} {{ job.manifest_branch }} -- cgit v1.2.3 From e179c23440e814a5e31b75639767da66dff5598b Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 26 Apr 2018 13:32:13 +0900 Subject: Display recent two days' builds. Test: dev_appserver.py app.yaml worker.yaml Bug: 78604394 --- gae/webapp/src/dashboard/build_list.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gae/webapp/src/dashboard/build_list.py b/gae/webapp/src/dashboard/build_list.py index 2932fc0..491f10a 100644 --- a/gae/webapp/src/dashboard/build_list.py +++ b/gae/webapp/src/dashboard/build_list.py @@ -15,6 +15,8 @@ # limitations under the License. # +import datetime + from webapp.src.handlers import base from webapp.src.proto import model @@ -30,7 +32,9 @@ def ReadBuildInfo(target_branch=""): a dict containing device build information, a dict containing gsi build information. """ - build_query = model.BuildModel.query() + build_query = model.BuildModel.query( + model.BuildModel.timestamp > + datetime.datetime.now() - datetime.timedelta(days=2)) builds = build_query.fetch() test_builds = {} -- cgit v1.2.3 From 99c4acca7c62dea3336e34e2b6143920b73e3b39 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 26 Apr 2018 21:22:39 +0900 Subject: Strengthen unit test codes. These hadn't written with email_util module, so it occurs unexpected failures on unit-test. Test: ./script/run-unittest.sh Bug: 77617865 Change-Id: Iad70bfae27b3ea2414db9e0582ae20e04352b1cb --- gae/webapp/src/scheduler/job_heartbeat_test.py | 3 +-- gae/webapp/src/scheduler/schedule_worker_test.py | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gae/webapp/src/scheduler/job_heartbeat_test.py b/gae/webapp/src/scheduler/job_heartbeat_test.py index 49c8927..ea65906 100644 --- a/gae/webapp/src/scheduler/job_heartbeat_test.py +++ b/gae/webapp/src/scheduler/job_heartbeat_test.py @@ -25,6 +25,7 @@ except ImportError: from webapp.src import vtslab_status as Status from webapp.src.proto import model +from webapp.src.scheduler import job_heartbeat from webapp.src.scheduler import schedule_worker from google.appengine.ext import ndb @@ -50,8 +51,6 @@ class JobHeartbeatTest(unittest.TestCase): self.testbed.init_mail_stub() # Clear cache between tests. ndb.get_context().clear_cache() - # import job_heartbeat after setting app_id. - from webapp.src.scheduler import job_heartbeat # Mocking PeriodicJobHeartBeat and essential methods. self.job_heartbeat = job_heartbeat.PeriodicJobHeartBeat(mock.Mock()) self.job_heartbeat.response = mock.Mock() diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index a350903..418b9ad 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -69,12 +69,17 @@ class ScheduleHandlerTest(unittest.TestCase): schedule.device = ["test_lab1/product1"] schedule.shards = 1 schedule.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + schedule.gsi_branch = "gsi_branch" + schedule.gsi_build_target = "gsi_build_target" + schedule.test_branch = "test_branch" + schedule.test_build_target = "test_build_target" schedule.require_signed_device_build = False schedule.put() lab = model.LabModel() lab.name = "test_lab1" lab.hostname = "test_lab1_host1" + lab.owner = "test@google.com" lab.put() device = model.DeviceModel() -- cgit v1.2.3 From 924959425be03f846b1a023a4b55ccc7e666427d Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 30 Apr 2018 15:42:50 +0900 Subject: Display 72 hours statistics. Test: ./dev_appserver.py app.yaml worker.yaml Bug: 78823610 --- gae/webapp/src/dashboard/job_list.py | 8 ++++++++ gae/webapp/static/job.html | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index 7eba6ec..16d1c46 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -116,17 +116,21 @@ class JobPage(JobBase): now = datetime.datetime.now() stats_all = JobStats() stats_24hrs = JobStats() + stats_72hrs = JobStats() if jobs: for job in jobs: self._UpdateStats(stats_all, job) if now - job.timestamp <= datetime.timedelta(hours=24): self._UpdateStats(stats_24hrs, job) + if now - job.timestamp <= datetime.timedelta(hours=72): + self._UpdateStats(stats_72hrs, job) template_values = { "jobs": sorted(jobs, key=lambda x: x.timestamp, reverse=True), "stats_all": stats_all, "stats_24hrs": stats_24hrs, + "stats_72hrs": stats_72hrs, "test_type_text": test_type_text } @@ -236,11 +240,14 @@ class CreateJobPage(JobBase): now = datetime.datetime.now() stats_all = JobStats() stats_24hrs = JobStats() + stats_72hrs = JobStats() if jobs: for job in jobs: self._UpdateStats(stats_all, job) if now - job.timestamp <= datetime.timedelta(hours=24): self._UpdateStats(stats_24hrs, job) + if now - job.timestamp <= datetime.timedelta(hours=72): + self._UpdateStats(stats_72hrs, job) template_values = { "message": message, @@ -248,6 +255,7 @@ class CreateJobPage(JobBase): reverse=True), "stats_all": stats_all, "stats_24hrs": stats_24hrs, + "stats_72hrs": stats_72hrs, "test_type_text": test_type_text } diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index 3e56fb9..ff0228c 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -115,6 +115,39 @@ {% else %} n/a {% endif %} +
72 Hours + {{ stats_72hrs.created }} + + {% if stats_72hrs.created %} + {{ "%0.2f" % (stats_72hrs.completed * 100 / stats_72hrs.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_72hrs.created %} + {{ "%0.2f" % ((stats_72hrs.running + stats_72hrs.ready) * 100 / stats_72hrs.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_72hrs.created %} + {{ "%0.2f" % (stats_72hrs.boot_error * 100 / stats_72hrs.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_72hrs.created %} + {{ "%0.2f" % (stats_72hrs.infra_error * 100 / stats_72hrs.created)|float }}% + {% else %} + n/a + {% endif %} + + {% if stats_72hrs.created %} + {{ "%0.2f" % (stats_72hrs.expired * 100 / stats_72hrs.created)|float }}% + {% else %} + n/a + {% endif %}
All {{ stats_all.created }} -- cgit v1.2.3 From a305b8f6ceb56de25f76fe24fe6671f14346112e Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 26 Apr 2018 21:12:42 +0900 Subject: Update email_util. This change is to include schedule suspension notification, and to prevent circular import issues. Test: dev_appserver.py app.yaml worker.yaml Bug: 73845606 Change-Id: I8a9393b9eff5d6c9caf191cdf8c3671ba30e08f7 --- gae/webapp/src/utils/email_util.py | 105 ++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/gae/webapp/src/utils/email_util.py b/gae/webapp/src/utils/email_util.py index fd327f0..2ef795e 100644 --- a/gae/webapp/src/utils/email_util.py +++ b/gae/webapp/src/utils/email_util.py @@ -23,11 +23,10 @@ from google.appengine.api import app_identity from google.appengine.api import mail from webapp.src import vtslab_status as Status -from webapp.src.handlers import base from webapp.src.proto import model +from webapp.src.utils import datetime_util -SENDER_ADDRESS = "noreply@{}.appspotmail.com".format( - app_identity.get_application_id()) +SENDER_ADDRESS = "noreply@{}.appspotmail.com" SEND_NOTIFICATION_FOOTER = ( "You are receiving this email because you are " @@ -44,6 +43,15 @@ SEND_JOB_NOTIFICATION_TITLE = ("[VTS lab] Job error has been occurred in " "lab {} ({})") SEND_JOB_NOTIFICATION_HEADER = ("Jobs in lab {} have been completed " "unexpectedly.") +SEND_SCHEDULE_SUSPENSION_NOTIFICATION_TITLE = ( + "[VTS lab] A job schedule has been {}. ({})") +SEND_SCHEDULE_SUSPENSION_NOTIFICATION_HEADER = ("The below job schedule has " + "been {}.") +SEND_SCHEDULE_SUSPENSION_NOTIFICATION_FOOTER = ( + "You are receiving this email because one or more labs which you are " + "listed as an owner or an administrator are affected.\nIf you received " + "this email by mistake, please send an email to VTS Lab infra development " + "team. Thank you.") def send_device_notification(devices): @@ -55,7 +63,8 @@ def send_device_notification(devices): """ for lab in devices: email_message = mail.EmailMessage() - email_message.sender = SENDER_ADDRESS + email_message.sender = SENDER_ADDRESS.format( + app_identity.get_application_id()) try: email_message.to = verify_recipient_address( devices[lab]["_recipients"]) @@ -64,7 +73,7 @@ def send_device_notification(devices): continue email_message.subject = SEND_DEVICE_NOTIFICATION_TITLE.format( lab, - base.GetTimeWithTimezone( + datetime_util.GetTimeWithTimezone( datetime.datetime.now()).strftime("%Y-%m-%d")) message = "" message += SEND_DEVICE_NOTIFICATION_HEADER.format(lab) @@ -126,7 +135,8 @@ def send_job_notification(jobs): for lab in labs_to_alert: email_message = mail.EmailMessage() - email_message.sender = SENDER_ADDRESS + email_message.sender = SENDER_ADDRESS.format( + app_identity.get_application_id()) try: email_message.to = verify_recipient_address( labs_to_alert[lab]["_recipients"]) @@ -135,7 +145,7 @@ def send_job_notification(jobs): continue email_message.subject = SEND_JOB_NOTIFICATION_TITLE.format( lab, - base.GetTimeWithTimezone( + datetime_util.GetTimeWithTimezone( datetime.datetime.now()).strftime("%Y-%m-%d")) message = "" message += SEND_JOB_NOTIFICATION_HEADER.format(lab) @@ -155,7 +165,7 @@ def send_job_notification(jobs): message += "test: branch - {}, target - {}, build_id - {}\n".format( job.test_branch, job.test_build_target, job.test_build_id) message += "job created: {}\n".format( - base.GetTimeWithTimezone( + datetime_util.GetTimeWithTimezone( job.timestamp).strftime("%Y-%m-%d %H:%M:%S %Z")) message += "job status: {}\n".format([ key for key, value in Status.JOB_STATUS_DICT.items() @@ -173,6 +183,85 @@ def send_job_notification(jobs): logging.exception(e) +def send_schedule_suspension_notification(schedule): + """Sends notification when a schedule is suspended, or resumed. + + Args: + schedule: a ScheduleModel entity. + """ + if not schedule: + return + + if not schedule.device: + return + + email_message = mail.EmailMessage() + email_message.sender = SENDER_ADDRESS.format( + app_identity.get_application_id()) + + lab_names = [] + for device in schedule.device: + if not "/" in device: + continue + lab_name = device.split("/")[0] + lab_names.append(lab_name) + + recipients = [] + for lab_name in lab_names: + lab_query = model.LabModel.query(model.LabModel.name == lab_name) + labs = lab_query.fetch() + if labs: + lab = labs[0] + if lab.owner not in recipients: + recipients.append(lab.owner) + recipients.extend([x for x in lab.admin if x not in recipients]) + else: + logging.warning( + "Could not find a lab model for lab {}".format(lab_name)) + + try: + email_message.to = verify_recipient_address(recipients) + except ValueError as e: + logging.error(e) + return + + status_text = "suspended" if schedule.suspended else "resumed" + email_message.subject = SEND_SCHEDULE_SUSPENSION_NOTIFICATION_TITLE.format( + status_text, + datetime_util.GetTimeWithTimezone( + datetime.datetime.now()).strftime("%Y-%m-%d")) + message = "" + message += SEND_SCHEDULE_SUSPENSION_NOTIFICATION_HEADER.format(status_text) + message += "\n\n" + message += "\n\ndevices\n" + message += "\n".join(schedule.device) + message += "\n\ndevice branch\n" + message += schedule.manifest_branch + message += "\n\ndevice build target\n" + message += schedule.build_target + message += "\n\ngsi branch\n" + message += schedule.gsi_branch + message += "\n\ngsi build target\n" + message += schedule.gsi_build_target + message += "\n\ntest branch\n" + message += schedule.test_branch + message += "\n\ntest build target\n" + message += schedule.test_build_target + message += "\n\n" + message += ("Please see the details in the following link: " + "http://{}.appspot.com/schedule".format( + app_identity.get_application_id())) + message += "\n\n\n\n" + message += SEND_SCHEDULE_SUSPENSION_NOTIFICATION_FOOTER + + try: + email_message.body = message + email_message.check_initialized() + email_message.send() + except mail.MissingRecipientError as e: + logging.exception(e) + + def verify_recipient_address(address): """Verifies recipients address. -- cgit v1.2.3 From befbe61a5e6fbb157fb4a771b56f37f779303b5e Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 25 Apr 2018 16:52:51 +0900 Subject: Add suspend and resume feature for schedule. Test: dev_appserver.py app.yaml worker.yaml Bug: 73845606 Change-Id: Ifc7c98989835ef18508f7f0682e915163a513152 --- gae/index.yaml | 2 + gae/webapp/src/dashboard/schedule_list.py | 19 ++ gae/webapp/src/endpoint/job_queue.py | 3 + gae/webapp/src/endpoint/schedule_info.py | 2 + gae/webapp/src/proto/model.py | 11 +- gae/webapp/src/scheduler/job_heartbeat.py | 3 + gae/webapp/src/scheduler/schedule_worker.py | 7 +- gae/webapp/src/tasks/indexing.py | 4 + gae/webapp/src/utils/model_util.py | 56 ++++++ gae/webapp/src/utils/model_util_test.py | 274 ++++++++++++++++++++++++++++ gae/webapp/static/schedule.html | 7 + 11 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 gae/webapp/src/utils/model_util.py create mode 100644 gae/webapp/src/utils/model_util_test.py diff --git a/gae/index.yaml b/gae/index.yaml index ecb75e8..61cf10b 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -43,6 +43,8 @@ indexes: - name: test_pab_account_id - name: timestamp - name: children_jobs + - name: suspended + - name: error_count - kind: LabModel ancestor: no diff --git a/gae/webapp/src/dashboard/schedule_list.py b/gae/webapp/src/dashboard/schedule_list.py index fdc1d80..830ebe1 100644 --- a/gae/webapp/src/dashboard/schedule_list.py +++ b/gae/webapp/src/dashboard/schedule_list.py @@ -15,8 +15,11 @@ # limitations under the License. # +from google.appengine.ext import ndb + from webapp.src.handlers import base from webapp.src.proto import model +from webapp.src.utils import email_util class SchedulePage(base.BaseHandler): @@ -26,6 +29,22 @@ class SchedulePage(base.BaseHandler): """Generates an HTML page based on the task schedules kept in DB.""" self.template = "schedule.html" + resume_key = self.request.get("resume") + if resume_key: + schedule_key = ndb.key.Key(urlsafe=resume_key) + schedule = schedule_key.get() + schedule.suspended = False + schedule.put() + email_util.send_schedule_suspension_notification(schedule) + + suspend_key = self.request.get("suspend") + if suspend_key: + schedule_key = ndb.key.Key(urlsafe=suspend_key) + schedule = schedule_key.get() + schedule.suspended = True + schedule.put() + email_util.send_schedule_suspension_notification(schedule) + toggle = self.request.get("schedule_enable_status_toggle", default_value="0") schedule_control = model.ScheduleControlModel.query() diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 28f09f8..2cfefe0 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -23,6 +23,7 @@ from protorpc import remote from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.utils import email_util +from webapp.src.utils import model_util JOB_QUEUE_RESOURCE = endpoints.ResourceContainer(model.JobMessage) GCS_URL_PREFIX = "gs://" @@ -212,8 +213,10 @@ class JobQueueApi(remote.Service): job.infra_log_url = request.infra_log_url else: logging.debug("[heartbeat] Wrong infra_log_url address.") + job.heartbeat_stamp = datetime.datetime.now() job.put() + model_util.UpdateParentSchedule(job, request.status) return model.JobLeaseResponse( return_code=model.ReturnCodeMessage.SUCCESS, jobs=job_messages) diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index 4ec084e..2839e7b 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -82,6 +82,8 @@ class ScheduleInfoApi(remote.Service): schedule.test_pab_account_id = request.test_pab_account_id schedule.timestamp = datetime.datetime.now() schedule.schedule_type = "test" + schedule.error_count = 0 + schedule.suspended = False schedule.put() return model.DefaultResponse( diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 526fb3c..8219d21 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -87,6 +87,8 @@ class ScheduleModel(ndb.Model): retry_count = ndb.IntegerProperty() children_jobs = ndb.KeyProperty(kind="JobModel", repeated=True) + error_count = ndb.IntegerProperty() + suspended = ndb.BooleanProperty() class ScheduleControlInfoMessage(messages.Message): @@ -156,8 +158,7 @@ class LabHostInfoMessage(messages.Message): hostname = messages.StringField(1, repeated=False) ip = messages.StringField(2, repeated=False) script = messages.StringField(3) - device = messages.MessageField( - LabDeviceInfoMessage, 4, repeated=True) + device = messages.MessageField(LabDeviceInfoMessage, 4, repeated=True) vtslab_version = messages.StringField(5) @@ -166,8 +167,7 @@ class LabInfoMessage(messages.Message): name = messages.StringField(1) owner = messages.StringField(2) admin = messages.StringField(4, repeated=True) - host = messages.MessageField( - LabHostInfoMessage, 3, repeated=True) + host = messages.MessageField(LabHostInfoMessage, 3, repeated=True) class DeviceModel(ndb.Model): @@ -191,8 +191,7 @@ class DeviceInfoMessage(messages.Message): class HostInfoMessage(messages.Message): """A message for representing an individual host entry.""" hostname = messages.StringField(1) - devices = messages.MessageField( - DeviceInfoMessage, 2, repeated=True) + devices = messages.MessageField(DeviceInfoMessage, 2, repeated=True) class JobModel(ndb.Model): diff --git a/gae/webapp/src/scheduler/job_heartbeat.py b/gae/webapp/src/scheduler/job_heartbeat.py index b7e96fc..d795714 100644 --- a/gae/webapp/src/scheduler/job_heartbeat.py +++ b/gae/webapp/src/scheduler/job_heartbeat.py @@ -25,6 +25,7 @@ from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.utils import email_util from webapp.src.utils import logger +from webapp.src.utils import model_util JOB_RESPONSE_TIMEOUT_SECONDS = 300 @@ -65,6 +66,8 @@ class PeriodicJobHeartBeat(webapp2.RequestHandler): job.hostname, job.device, job.test_name)) job.status = Status.JOB_STATUS_DICT["infra-err"] lost_jobs_to_put.append(job) + model_util.UpdateParentSchedule(job, + Status.JOB_STATUS_DICT["infra-err"]) device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 818884e..034e89a 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -22,6 +22,7 @@ import re from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.utils import logger +from webapp.src.utils import model_util import webapp2 MAX_LOG_CHARACTERS = 10000 # maximum number of characters per each log @@ -162,7 +163,8 @@ class ScheduleHandler(webapp2.RequestHandler): def post(self): self.logger.Clear() - schedule_query = model.ScheduleModel.query() + schedule_query = model.ScheduleModel.query( + model.ScheduleModel.suspended != True) schedules = schedule_query.fetch() if schedules: @@ -238,6 +240,7 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.timestamp = datetime.datetime.now() new_job_key = new_job.put() schedule.children_jobs.append(new_job_key) + schedule.put() self.logger.Println("NEW JOB") else: self.logger.Println("NO BUILD FOUND") @@ -247,6 +250,7 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.timestamp = datetime.datetime.now() new_job_key = new_job.put() schedule.children_jobs.append(new_job_key) + schedule.put() self.logger.Println("NEW JOB - GCS") else: self.logger.Println("Unexpected storage type (%s)." % @@ -314,6 +318,7 @@ class ScheduleHandler(webapp2.RequestHandler): for job in outdated_ready_jobs: job.status = Status.JOB_STATUS_DICT["infra-err"] job.put() + model_util.UpdateParentSchedule(job, job.status) outdated_leased_jobs = [ x for x in outdated_jobs diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index 94a8d08..935bf17 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -152,6 +152,10 @@ class IndexingHandler(webapp2.RequestHandler): to_put.append(parent_schedule) elif model_type == "schedule": + if entity.error_count is None: + entity.error_count = 0 + if entity.suspended is None: + entity.suspended = False if entity.build_storage_type is None: entity.build_storage_type = Status.STORAGE_TYPE_DICT[ "PAB"] diff --git a/gae/webapp/src/utils/model_util.py b/gae/webapp/src/utils/model_util.py new file mode 100644 index 0000000..66c575e --- /dev/null +++ b/gae/webapp/src/utils/model_util.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from webapp.src import vtslab_status as Status +from webapp.src.utils import email_util + + +def UpdateParentSchedule(job, status): + """Updates a parent schedule of the given job with status. + + Args: + job: a JobModel entity. + status: an integer, job status value. + """ + if status not in [ + Status.JOB_STATUS_DICT["complete"], + Status.JOB_STATUS_DICT["infra-err"], + Status.JOB_STATUS_DICT["expired"], + Status.JOB_STATUS_DICT["bootup-err"] + ]: + return + + if job.parent_schedule: + schedule = job.parent_schedule.get() + if schedule: + previous_suspended = schedule.suspended + if schedule.error_count is None: + schedule.error_count = 0 + if status == Status.JOB_STATUS_DICT["complete"]: + schedule.error_count = 0 + schedule.suspended = False + elif status in [ + Status.JOB_STATUS_DICT["infra-err"], + Status.JOB_STATUS_DICT["expired"], + Status.JOB_STATUS_DICT["bootup-err"] + ]: + schedule.error_count += 1 + if schedule.error_count >= 3: + schedule.suspended = True + schedule.put() + if previous_suspended != schedule.suspended: + email_util.send_schedule_suspension_notification(schedule) diff --git a/gae/webapp/src/utils/model_util_test.py b/gae/webapp/src/utils/model_util_test.py new file mode 100644 index 0000000..f86e2b8 --- /dev/null +++ b/gae/webapp/src/utils/model_util_test.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import datetime +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.proto import model +from webapp.src.scheduler import schedule_worker +from webapp.src.utils import model_util + +from google.appengine.ext import ndb +from google.appengine.ext import testbed + + +class ModelTest(unittest.TestCase): + """Tests for PeriodicJobHeartBeat cron class. + + Attributes: + testbed: A Testbed instance which provides local unit testing. + """ + + def setUp(self): + """Initializes test""" + # Create the Testbed class instance and initialize service stubs. + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.setup_env(app_id="vtslab-schedule-unittest") + self.testbed.init_datastore_v3_stub() + self.testbed.init_memcache_stub() + self.testbed.init_mail_stub() + # Clear cache between tests. + ndb.get_context().clear_cache() + # import job_heartbeat after setting app_id. + + def tearDown(self): + self.testbed.deactivate() + + def testJobAndScheduleModel(self): + """Asserts JobModel and ScheduleModel. + + When JobModel's status is changed, ScheduleModel's error_count is + changed based on the status. This should not be applied before JobModel + entity is updated to Datastore. + """ + # schedule information + priority = "top" + period = 360 + build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + manifest_branch = "manifest_branch" + build_target = "device_build_target-user" + pab_account_id = "1234567890" + shards = 1 + retry_count = 1 + gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + gsi_branch = "gsi_branch" + gsi_build_target = "gsi_build_target-user" + gsi_pab_account_id = "1234567890" + test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + test_branch = "test_branch" + test_build_target = "test_build_target-user" + test_pab_account_id = "1234567890" + + lab_name = "test_lab" + host_name = "test_host" + device_name = "device" + + # create a device build + build = model.BuildModel() + build.manifest_branch = manifest_branch + build.build_id = "1000000" + build.build_target = "device_build_target" + build.build_type = "user" + build.artifact_type = "device" + build.timestamp = datetime.datetime.now() + build.signed = False + build.put() + + # create a gsi build + build = model.BuildModel() + build.manifest_branch = gsi_branch + build.build_id = "2000000" + build.build_target = "gsi_build_target" + build.build_type = "user" + build.artifact_type = "gsi" + build.timestamp = datetime.datetime.now() + build.signed = False + build.put() + + # create a test build + build = model.BuildModel() + build.manifest_branch = test_branch + build.build_id = "3000000" + build.build_target = "test_build_target" + build.build_type = "user" + build.artifact_type = "test" + build.timestamp = datetime.datetime.now() + build.signed = False + build.put() + + # create a lab + lab = model.LabModel() + lab.name = lab_name + lab.hostname = host_name + lab.owner = "test@google.com" + lab.put() + + # create a device + device = model.DeviceModel() + device.hostname = host_name + device.product = device_name + device.serial = "serial" + device.status = Status.DEVICE_STATUS_DICT["fastboot"] + device.scheduling_status = ( + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) + device.timestamp = datetime.datetime.now() + device.put() + + # create a schedule + schedule = model.ScheduleModel() + schedule.priority = priority + schedule.test_name = "test/{}".format(device_name) + schedule.period = period + schedule.build_storage_type = build_storage_type + schedule.manifest_branch = manifest_branch + schedule.build_target = build_target + schedule.device_pab_account_id = pab_account_id + schedule.shards = shards + schedule.retry_count = retry_count + schedule.gsi_storage_type = gsi_storage_type + schedule.gsi_branch = gsi_branch + schedule.gsi_build_target = gsi_build_target + schedule.gsi_pab_account_id = gsi_pab_account_id + schedule.test_storage_type = test_storage_type + schedule.test_branch = test_branch + schedule.test_build_target = test_build_target + schedule.test_pab_account_id = test_pab_account_id + schedule.device = [] + schedule.device.append("{}/{}".format(lab_name, device_name)) + schedule.put() + + schedule = model.ScheduleModel.query().fetch()[0] + schedule.put() + + # Mocking ScheduleHandler and essential methods. + scheduler = schedule_worker.ScheduleHandler(mock.Mock()) + scheduler.response = mock.Mock() + scheduler.response.write = mock.Mock() + + print("\nCreating a job...") + scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(1, len(jobs)) + + print("Occurring infra error...") + job = jobs[0] + job.status = Status.JOB_STATUS_DICT["infra-err"] + parent_schedule = job.parent_schedule.get() + parent_from_db = model.ScheduleModel.query().fetch()[0] + + # in test error_count could be None but in real there will be no None. + self.assertNotEqual(1, parent_schedule.error_count) + self.assertNotEqual(1, parent_from_db.error_count) + + # error count should be changed after put + job.put() + model_util.UpdateParentSchedule(job, job.status) + self.assertEqual(1, parent_schedule.error_count) + self.assertEqual(1, parent_from_db.error_count) + + print("Suspending a job...") + for num in xrange(2): + jobs = model.JobModel.query().fetch() + for job in jobs: + job.timestamp = datetime.datetime.now() - datetime.timedelta( + minutes=(period + 10)) + job.put() + + parent_from_db = model.ScheduleModel.query().fetch()[0] + self.assertEqual(1 + num, parent_schedule.error_count) + self.assertEqual(1 + num, parent_from_db.error_count) + + # reset a device manually to re-schedule + device = model.DeviceModel.query().fetch()[0] + device.status = Status.DEVICE_STATUS_DICT["fastboot"] + device.scheduling_status = ( + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) + device.timestamp = datetime.datetime.now() + device.put() + + scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(2 + num, len(jobs)) + + ready_jobs = model.JobModel.query( + model.JobModel.status == Status.JOB_STATUS_DICT[ + "ready"]).fetch() + self.assertEqual(1, len(ready_jobs)) + + ready_job = ready_jobs[0] + ready_job.status = Status.JOB_STATUS_DICT["infra-err"] + parent_schedule = ready_job.parent_schedule.get() + parent_from_db = model.ScheduleModel.query().fetch()[0] + self.assertEqual(1 + num, parent_schedule.error_count) + self.assertEqual(1 + num, parent_from_db.error_count) + + # # error count should be changed after put + ready_job.put() + model_util.UpdateParentSchedule(ready_job, ready_job.status) + self.assertEqual(2 + num, parent_schedule.error_count) + self.assertEqual(2 + num, parent_from_db.error_count) + + print("Asserting a schedule's suspend status...") + # after three errors the schedule should be suspended. + schedule_from_db = model.ScheduleModel.query().fetch()[0] + schedule_from_db.put() + self.assertEqual(3, schedule_from_db.error_count) + self.assertEqual(True, schedule_from_db.suspended) + + # reset a device manually to re-schedule + device = model.DeviceModel.query().fetch()[0] + device.status = Status.DEVICE_STATUS_DICT["fastboot"] + device.scheduling_status = ( + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) + device.timestamp = datetime.datetime.now() + device.put() + + print("Asserting that job creation is blocked...") + jobs = model.JobModel.query().fetch() + self.assertEquals(3, len(jobs)) + + for job in jobs: + job.timestamp = datetime.datetime.now() - datetime.timedelta( + minutes=(period + 10)) + job.put() + + scheduler.post() + + # a job should not be created. + jobs = model.JobModel.query().fetch() + self.assertEquals(3, len(jobs)) + + print("Asserting that job creation is allowed after resuming...") + schedule_from_db = model.ScheduleModel.query().fetch()[0] + schedule_from_db.suspended = False + schedule_from_db.put() + + scheduler.post() + + jobs = model.JobModel.query().fetch() + self.assertEquals(4, len(jobs)) + + +if __name__ == "__main__": + unittest.main() diff --git a/gae/webapp/static/schedule.html b/gae/webapp/static/schedule.html index 2c0539b..2e5069d 100644 --- a/gae/webapp/static/schedule.html +++ b/gae/webapp/static/schedule.html @@ -82,6 +82,7 @@ param priority timestamp + resume/suspend
+ {% if schedule.suspended %} + suspended + {% else %} + error(s): {{ schedule.error_count }} + {% endif %}
-- cgit v1.2.3 From 0f8938b2b17530b47155103a75842a7e78baa49d Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 3 May 2018 13:14:32 +0900 Subject: Refactor timezone method into a separated module. Test: dev_appserver.py app.yaml worker.yaml Bug: 73845606 Change-Id: Ifb2ee232b2ff4637a8af76658e3ed6b8d7574541 --- gae/webapp/src/handlers/base.py | 27 +++--------------------- gae/webapp/src/utils/datetime_util.py | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 gae/webapp/src/utils/datetime_util.py diff --git a/gae/webapp/src/handlers/base.py b/gae/webapp/src/handlers/base.py index bc39879..2862ecb 100644 --- a/gae/webapp/src/handlers/base.py +++ b/gae/webapp/src/handlers/base.py @@ -20,35 +20,14 @@ import logging import os import urlparse -import pytz +from google.appengine.api import users import stripe import webapp2 -from google.appengine.api import users from webapp2_extras import jinja2 as wa2_jinja2 from webapp2_extras import sessions import errors - - -def GetTimeWithTimezone(dt, timezone="US/Pacific"): - """Converts timezone of datetime.datetime() instance. - - Args: - dt: datetime.datetime() instance. - timezone: a string representing timezone listed in TZ database. - - Returns: - datetime.datetime() instance with the given timezone. - """ - if not dt: - return None - utc_time = dt.replace(tzinfo=pytz.utc) - try: - converted_time = utc_time.astimezone(pytz.timezone(timezone)) - except pytz.UnknownTimeZoneError as e: - logging.exception(e) - converted_time = dt - return converted_time +from webapp.src.utils import datetime_util class BaseHandler(webapp2.RequestHandler): @@ -201,7 +180,7 @@ class BaseHandler(webapp2.RequestHandler): 'user': user, 'url': url, 'url_linktext': url_linktext, - "convert_time": GetTimeWithTimezone + "convert_time": datetime_util.GetTimeWithTimezone }) if 'preload' not in resp: diff --git a/gae/webapp/src/utils/datetime_util.py b/gae/webapp/src/utils/datetime_util.py new file mode 100644 index 0000000..a1cff67 --- /dev/null +++ b/gae/webapp/src/utils/datetime_util.py @@ -0,0 +1,39 @@ +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import pytz + + +def GetTimeWithTimezone(dt, timezone="US/Pacific"): + """Converts timezone of datetime.datetime() instance. + + Args: + dt: datetime.datetime() instance. + timezone: a string representing timezone listed in TZ database. + + Returns: + datetime.datetime() instance with the given timezone. + """ + if not dt: + return None + utc_time = dt.replace(tzinfo=pytz.utc) + try: + converted_time = utc_time.astimezone(pytz.timezone(timezone)) + except pytz.UnknownTimeZoneError as e: + logging.exception(e) + converted_time = dt + return converted_time -- cgit v1.2.3 From 09275458b7d1351b6f29992624a472585656f387 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Fri, 4 May 2018 11:46:10 +0900 Subject: Reserve devices when fetching from GCS. Test: mma Bug: 79215639 --- gae/webapp/src/scheduler/schedule_worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 034e89a..7bffd42 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -246,6 +246,7 @@ class ScheduleHandler(webapp2.RequestHandler): self.logger.Println("NO BUILD FOUND") elif new_job.build_storage_type == ( Status.STORAGE_TYPE_DICT["GCS"]): + self.ReserveDevices(target_device_serials) new_job.status = Status.JOB_STATUS_DICT["ready"] new_job.timestamp = datetime.datetime.now() new_job_key = new_job.put() -- cgit v1.2.3 From 973e3efb9a1957ea9ee0767119ef9e9a2c75e4ae Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 9 May 2018 10:02:33 +0900 Subject: Specify a build id for all artifact types. Test: vtslab-schedule-dev.appspot.com Bug: 73557356 Change-Id: Ifb243a0e7ca9610327ed0c1ad1f6f39f5eb13338 --- gae/webapp/src/scheduler/schedule_worker.py | 109 +++++++++++++++++----------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 7bffd42..2225b9e 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -124,40 +124,41 @@ class ScheduleHandler(webapp2.RequestHandler): "reserved"] device.put() - def FindBuildId(self, new_job): - """Finds build ID for a new job. + def FindBuildId(self, artifact_type, manifest_branch, target, + signed=False): + """Finds a designated build ID. Args: - new_job: JobModel, a new job. + artifact_type: a string, build artifact type. + manifest_branch: a string, build manifest branch. + target: a string which build target and type are joined by '-'. + signed: a boolean to get a signed build. Return: string, build ID found. """ build_id = "" + if "-" in target: + build_target, build_type = target.split("-") + else: + build_target = target + build_type = "" + if not artifact_type or not manifest_branch or not build_target: + self.logger.Println("The argument format is invalid.") + return build_id build_query = model.BuildModel.query( - model.BuildModel.manifest_branch == new_job.manifest_branch) + model.BuildModel.artifact_type == artifact_type, + model.BuildModel.manifest_branch == manifest_branch, + model.BuildModel.build_target == build_target, + model.BuildModel.build_type == build_type) builds = build_query.fetch() if builds: - self.logger.Println("-- Find build ID") - # Remove builds if build_id info is none - build_id_filled = [x for x in builds if x.build_id] - sorted_list = sorted( - build_id_filled, key=lambda x: int(x.build_id), reverse=True) - filtered_list = [ - x for x in sorted_list - if (all( - hasattr(x, attrs) - for attrs in ["build_target", "build_type", "build_id"]) - and x.build_target and x.build_type) - ] - for device_build in filtered_list: - candidate_build_target = "-".join( - [device_build.build_target, device_build.build_type]) - if (new_job.build_target == candidate_build_target - and (not new_job.require_signed_device_build - or device_build.signed)): - build_id = device_build.build_id + self.logger.Println("-- Found build ID") + builds.sort(key=lambda x: x.build_id, reverse=True) + for build in builds: + if not signed or build.signed: + build_id = build.build_id break return build_id @@ -191,7 +192,6 @@ class ScheduleHandler(webapp2.RequestHandler): self.logger.Println("- Target device: %s" % target_device) self.logger.Println( "- Target serials: %s" % target_device_serials) - # TODO: update device status # create job and add. new_job = model.JobModel() @@ -230,32 +230,55 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.test_type = test_type new_job.build_id = "" - - if new_job.build_storage_type == ( - Status.STORAGE_TYPE_DICT["PAB"]): - new_job.build_id = self.FindBuildId(new_job) - if new_job.build_id: - self.ReserveDevices(target_device_serials) - new_job.status = Status.JOB_STATUS_DICT["ready"] - new_job.timestamp = datetime.datetime.now() - new_job_key = new_job.put() - schedule.children_jobs.append(new_job_key) - schedule.put() - self.logger.Println("NEW JOB") + new_job.gsi_build_id = "" + new_job.test_build_id = "" + for artifact_type in ["device", "gsi", "test"]: + if artifact_type == "device": + storage_type_text = "build_storage_type" + manifest_branch_text = "manifest_branch" + build_target_text = "build_target" + build_id_text = "build_id" + signed = new_job.require_signed_device_build else: - self.logger.Println("NO BUILD FOUND") - elif new_job.build_storage_type == ( - Status.STORAGE_TYPE_DICT["GCS"]): + storage_type_text = artifact_type + "_storage_type" + manifest_branch_text = artifact_type + "_branch" + build_target_text = artifact_type + "_build_target" + build_id_text = artifact_type + "_build_id" + signed = False + + manifest_branch = getattr(new_job, manifest_branch_text) + build_target = getattr(new_job, build_target_text) + storage_type = getattr(new_job, storage_type_text) + if storage_type == Status.STORAGE_TYPE_DICT["PAB"]: + build_id = self.FindBuildId( + artifact_type=artifact_type, + manifest_branch=manifest_branch, + target=build_target, + signed=signed) + elif storage_type == Status.STORAGE_TYPE_DICT["GCS"]: + # temp value to distinguish from empty values. + build_id = "gcs" + else: + build_id = "" + self.logger.Println( + "Unexpected storage type (%s)." % storage_type) + setattr(new_job, build_id_text, build_id) + + if (new_job.build_id and new_job.gsi_build_id + and new_job.test_build_id): + new_job.build_id = new_job.build_id.replace("gcs", "") + new_job.gsi_build_id = (new_job.gsi_build_id.replace( + "gcs", "")) + new_job.test_build_id = (new_job.test_build_id.replace( + "gcs", "")) self.ReserveDevices(target_device_serials) new_job.status = Status.JOB_STATUS_DICT["ready"] new_job.timestamp = datetime.datetime.now() new_job_key = new_job.put() schedule.children_jobs.append(new_job_key) schedule.put() - self.logger.Println("NEW JOB - GCS") - else: - self.logger.Println("Unexpected storage type (%s)." % - new_job.build_storage_type) + self.logger.Println("A new job has been created.") + self.logger.Unindent() self.logger.Println("Scheduling completed.") -- cgit v1.2.3 From 98178a7bd63ae475c4f9ef7ec77eb37e4ec42a47 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 9 May 2018 19:38:12 +0900 Subject: Ignore duplicated schedule updates. Test: vtslab-schedule-dev.appspot.com Bug: 73845606 --- gae/webapp/src/endpoint/schedule_info.py | 84 +++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index 2839e7b..f7c3dcc 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -56,35 +56,61 @@ class ScheduleInfoApi(remote.Service): name="set") def set(self, request): """Sets the schedule info based on `request`.""" - schedule = model.ScheduleModel() - schedule.manifest_branch = request.manifest_branch - schedule.build_storage_type = request.build_storage_type - if request.get_assigned_value("device_pab_account_id"): - schedule.device_pab_account_id = request.device_pab_account_id - schedule.build_target = request.build_target - schedule.test_name = request.test_name - schedule.require_signed_device_build = ( - request.require_signed_device_build) - schedule.period = request.period - schedule.priority = request.priority - schedule.device = request.device - schedule.shards = request.shards - schedule.param = request.param - schedule.retry_count = request.retry_count - schedule.gsi_storage_type = request.gsi_storage_type - schedule.gsi_branch = request.gsi_branch - schedule.gsi_build_target = request.gsi_build_target - schedule.gsi_vendor_version = request.gsi_vendor_version - schedule.gsi_pab_account_id = request.gsi_pab_account_id - schedule.test_storage_type = request.test_storage_type - schedule.test_branch = request.test_branch - schedule.test_build_target = request.test_build_target - schedule.test_pab_account_id = request.test_pab_account_id - schedule.timestamp = datetime.datetime.now() - schedule.schedule_type = "test" - schedule.error_count = 0 - schedule.suspended = False - schedule.put() + # device_pab_account_id, param are missing. + duplicated_schedule_query = model.ScheduleModel.query( + model.ScheduleModel.manifest_branch == request.manifest_branch, + model.ScheduleModel.build_storage_type == request.build_storage_type, + model.ScheduleModel.build_target == request.build_target, + model.ScheduleModel.test_name == request.test_name, + model.ScheduleModel.require_signed_device_build == ( + request.require_signed_device_build), + model.ScheduleModel.period == request.period, + model.ScheduleModel.priority == request.priority, + model.ScheduleModel.device.IN(request.device), + model.ScheduleModel.shards == request.shards, + model.ScheduleModel.retry_count == request.retry_count, + model.ScheduleModel.gsi_storage_type == request.gsi_storage_type, + model.ScheduleModel.gsi_branch == request.gsi_branch, + model.ScheduleModel.gsi_build_target == request.gsi_build_target, + model.ScheduleModel.gsi_vendor_version == request.gsi_vendor_version, + model.ScheduleModel.gsi_pab_account_id == request.gsi_pab_account_id, + model.ScheduleModel.test_storage_type == request.test_storage_type, + model.ScheduleModel.test_branch == request.test_branch, + model.ScheduleModel.test_build_target == request.test_build_target, + model.ScheduleModel.test_pab_account_id == request.test_pab_account_id + ) + duplicated_schedules = duplicated_schedule_query.fetch() + + if not duplicated_schedules: + schedule = model.ScheduleModel() + schedule.manifest_branch = request.manifest_branch + schedule.build_storage_type = request.build_storage_type + if request.get_assigned_value("device_pab_account_id"): + schedule.device_pab_account_id = request.device_pab_account_id + schedule.build_target = request.build_target + schedule.test_name = request.test_name + schedule.require_signed_device_build = ( + request.require_signed_device_build) + schedule.period = request.period + schedule.priority = request.priority + schedule.device = request.device + schedule.shards = request.shards + schedule.param = request.param + schedule.retry_count = request.retry_count + schedule.gsi_storage_type = request.gsi_storage_type + schedule.gsi_branch = request.gsi_branch + schedule.gsi_build_target = request.gsi_build_target + schedule.gsi_vendor_version = request.gsi_vendor_version + schedule.gsi_pab_account_id = request.gsi_pab_account_id + schedule.test_storage_type = request.test_storage_type + schedule.test_branch = request.test_branch + schedule.test_build_target = request.test_build_target + schedule.test_pab_account_id = request.test_pab_account_id + schedule.timestamp = datetime.datetime.now() + schedule.schedule_type = "test" + schedule.error_count = 0 + schedule.suspended = False + schedule.put() return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) -- cgit v1.2.3 From 1e471d218d540fa964b7e60c165bf5cf9d26f746 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Fri, 11 May 2018 18:37:18 +0900 Subject: Remove None type key from schedule's children job. Test: vtslab-schedule-test.appspot.com Bug: 77298544 Change-Id: Ie6c38be0707a5243c6e42b9230ec05086abaad9f --- gae/webapp/src/tasks/indexing.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index 935bf17..a86e839 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -159,6 +159,12 @@ class IndexingHandler(webapp2.RequestHandler): if entity.build_storage_type is None: entity.build_storage_type = Status.STORAGE_TYPE_DICT[ "PAB"] + # remove None children jobs. + if entity.children_jobs: + entity.children_jobs = [ + x for x in entity.children_jobs if x] + else: + entity.children_jobs = [] else: pass to_put.append(entity) -- cgit v1.2.3 From 97ddbea66dd6239f6c3941de0812ba34d5186d53 Mon Sep 17 00:00:00 2001 From: Hyunwoo Ko Date: Wed, 16 May 2018 15:36:30 +0900 Subject: Adjust scheduler to be able to process image_package_repo_base. Test: mma Bug: 79717764 --- gae/webapp/src/endpoint/job_queue.py | 1 + gae/webapp/src/endpoint/schedule_info.py | 4 +++- gae/webapp/src/proto/model.py | 7 ++++++- gae/webapp/src/scheduler/schedule_worker.py | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 2cfefe0..285a593 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -102,6 +102,7 @@ class JobQueueApi(remote.Service): job_message.test_build_id = job.test_build_id job_message.test_pab_account_id = job.test_pab_account_id job_message.test_type = job.test_type + job_message.image_package_repo_base = job.image_package_repo_base device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index f7c3dcc..8006c5b 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -77,7 +77,8 @@ class ScheduleInfoApi(remote.Service): model.ScheduleModel.test_storage_type == request.test_storage_type, model.ScheduleModel.test_branch == request.test_branch, model.ScheduleModel.test_build_target == request.test_build_target, - model.ScheduleModel.test_pab_account_id == request.test_pab_account_id + model.ScheduleModel.test_pab_account_id == request.test_pab_account_id, + model.ScheduleModel.image_package_repo_base == request.image_package_repo_base ) duplicated_schedules = duplicated_schedule_query.fetch() @@ -110,6 +111,7 @@ class ScheduleInfoApi(remote.Service): schedule.schedule_type = "test" schedule.error_count = 0 schedule.suspended = False + schedule.image_package_repo_base = request.image_package_repo_base schedule.put() return model.DefaultResponse( diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 8219d21..324264f 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -89,6 +89,7 @@ class ScheduleModel(ndb.Model): children_jobs = ndb.KeyProperty(kind="JobModel", repeated=True) error_count = ndb.IntegerProperty() suspended = ndb.BooleanProperty() + image_package_repo_base = ndb.StringProperty() class ScheduleControlInfoMessage(messages.Message): @@ -240,10 +241,12 @@ class JobModel(ndb.Model): parent_schedule = ndb.KeyProperty(kind="ScheduleModel") + image_package_repo_base = ndb.StringProperty() + class JobMessage(messages.Message): """A message for representing an individual job entry.""" - # Next ID = 30 + # Next ID = 31 test_type = messages.IntegerField(29) hostname = messages.StringField(1) @@ -284,6 +287,8 @@ class JobMessage(messages.Message): infra_log_url = messages.StringField(24) + image_package_repo_base = messages.StringField(30) + class ReturnCodeMessage(messages.Enum): """Enum for default return code.""" diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 2225b9e..eacb423 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -220,6 +220,8 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.test_build_target = schedule.test_build_target new_job.test_pab_account_id = schedule.test_pab_account_id new_job.parent_schedule = schedule.key + new_job.image_package_repo_base = ( + schedule.image_package_repo_base) # uses bit 0-1 to indicate version. test_type = GetTestVersionType(schedule.manifest_branch, -- cgit v1.2.3 From 6a6cc73e9cb11427e03822d4e458db5432476da4 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 16 May 2018 10:34:33 +0900 Subject: Check build id only when branch is specified. Test: dev server Bug: 73557356 --- gae/webapp/src/scheduler/schedule_worker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 2225b9e..4d36b70 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -264,8 +264,9 @@ class ScheduleHandler(webapp2.RequestHandler): "Unexpected storage type (%s)." % storage_type) setattr(new_job, build_id_text, build_id) - if (new_job.build_id and new_job.gsi_build_id - and new_job.test_build_id): + if ((not new_job.manifest_branch or new_job.build_id) and + (not new_job.gsi_branch or new_job.gsi_build_id) and + (not new_job.test_branch or new_job.test_build_id)): new_job.build_id = new_job.build_id.replace("gcs", "") new_job.gsi_build_id = (new_job.gsi_build_id.replace( "gcs", "")) -- cgit v1.2.3 From 71461d84271e71d511e766c5eed8eb8ed8af45f9 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Fri, 18 May 2018 06:30:29 +0900 Subject: Refer only existing fields of request Test: localhost with prod and dev configs Bug: 79717764 --- gae/webapp/src/endpoint/schedule_info.py | 81 +++++++++++++------------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index 8006c5b..08f1b57 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -56,62 +56,45 @@ class ScheduleInfoApi(remote.Service): name="set") def set(self, request): """Sets the schedule info based on `request`.""" - # device_pab_account_id, param are missing. - duplicated_schedule_query = model.ScheduleModel.query( - model.ScheduleModel.manifest_branch == request.manifest_branch, - model.ScheduleModel.build_storage_type == request.build_storage_type, - model.ScheduleModel.build_target == request.build_target, - model.ScheduleModel.test_name == request.test_name, - model.ScheduleModel.require_signed_device_build == ( - request.require_signed_device_build), - model.ScheduleModel.period == request.period, - model.ScheduleModel.priority == request.priority, - model.ScheduleModel.device.IN(request.device), - model.ScheduleModel.shards == request.shards, - model.ScheduleModel.retry_count == request.retry_count, - model.ScheduleModel.gsi_storage_type == request.gsi_storage_type, - model.ScheduleModel.gsi_branch == request.gsi_branch, - model.ScheduleModel.gsi_build_target == request.gsi_build_target, - model.ScheduleModel.gsi_vendor_version == request.gsi_vendor_version, - model.ScheduleModel.gsi_pab_account_id == request.gsi_pab_account_id, - model.ScheduleModel.test_storage_type == request.test_storage_type, - model.ScheduleModel.test_branch == request.test_branch, - model.ScheduleModel.test_build_target == request.test_build_target, - model.ScheduleModel.test_pab_account_id == request.test_pab_account_id, - model.ScheduleModel.image_package_repo_base == request.image_package_repo_base - ) - duplicated_schedules = duplicated_schedule_query.fetch() + request_fields = request.all_fields() + model_attrs = model.ScheduleModel.__dict__.items() + model_attr_names = [ + x[0] for x in model_attrs if not x[0].startswith("_") + ] + exist_on_both = [ + x for x in request_fields if x.name in model_attr_names + ] + + # check duplicates + exclusions = [ + "name", "schedule_type", "schedule", "param", "timestamp", + "children_jobs", "error_count", "suspended" + ] + # list of protorpc message fields. + duplicate_checklist = [ + x for x in exist_on_both if x.name not in exclusions + ] + query = model.ScheduleModel.query() + for field in duplicate_checklist: + if field.repeated: + query = query.filter( + getattr(model.ScheduleModel, field.name).IN( + request.get_assigned_value(field.name))) + else: + query = query.filter( + getattr(model.ScheduleModel, field.name) == + request.get_assigned_value(field.name)) + duplicated_schedules = query.fetch() if not duplicated_schedules: schedule = model.ScheduleModel() - schedule.manifest_branch = request.manifest_branch - schedule.build_storage_type = request.build_storage_type - if request.get_assigned_value("device_pab_account_id"): - schedule.device_pab_account_id = request.device_pab_account_id - schedule.build_target = request.build_target - schedule.test_name = request.test_name - schedule.require_signed_device_build = ( - request.require_signed_device_build) - schedule.period = request.period - schedule.priority = request.priority - schedule.device = request.device - schedule.shards = request.shards - schedule.param = request.param - schedule.retry_count = request.retry_count - schedule.gsi_storage_type = request.gsi_storage_type - schedule.gsi_branch = request.gsi_branch - schedule.gsi_build_target = request.gsi_build_target - schedule.gsi_vendor_version = request.gsi_vendor_version - schedule.gsi_pab_account_id = request.gsi_pab_account_id - schedule.test_storage_type = request.test_storage_type - schedule.test_branch = request.test_branch - schedule.test_build_target = request.test_build_target - schedule.test_pab_account_id = request.test_pab_account_id + for field in exist_on_both: + setattr(schedule, field.name, + request.get_assigned_value(field.name)) schedule.timestamp = datetime.datetime.now() schedule.schedule_type = "test" schedule.error_count = 0 schedule.suspended = False - schedule.image_package_repo_base = request.image_package_repo_base schedule.put() return model.DefaultResponse( -- cgit v1.2.3 From abd4dc1f57c2ac8a9bc627aefeb1f30cd5100252 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Fri, 18 May 2018 09:23:46 +0900 Subject: Prevent to add duplicated lab info. Test: localhost Bug: 73845606 --- gae/webapp/src/endpoint/lab_info.py | 54 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index bbd74fd..da590a0 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -58,28 +58,38 @@ class LabInfoApi(remote.Service): name="set") def set(self, request): """Sets the lab info based on `request`.""" - for host in request.host: - lab = model.LabModel() - lab.name = request.name - lab.owner = request.owner - lab.admin = request.admin - lab.hostname = host.hostname - lab.ip = host.ip - lab.script = host.script - devices = [] - null_device_count = 0 - if host.device: - for device in host.device: - devices.append("%s=%s" % (device.serial, device.product)) - if device.product == "null": - null_device_count += 1 - if devices: - lab.devices = ",".join(devices) - lab.timestamp = datetime.datetime.now() - lab.put() - - if null_device_count > 0: - host_info.AddNullDevices(host.hostname, null_device_count) + if "host" in [x.name for x in request.all_fields()]: + for host in request.host: + duplicate_query = model.LabModel.query( + model.LabModel.name == request.name, + model.LabModel.owner == request.owner, + model.LabModel.hostname == host.hostname + ) + duplicates = duplicate_query.fetch() + if duplicates: + lab = duplicates[0] + else: + lab = model.LabModel() + lab.name = request.name + lab.owner = request.owner + lab.admin = request.admin + lab.hostname = host.hostname + lab.ip = host.ip + lab.script = host.script + devices = [] + null_device_count = 0 + if host.device: + for device in host.device: + devices.append("%s=%s" % (device.serial, device.product)) + if device.product == "null": + null_device_count += 1 + if devices: + lab.devices = ",".join(devices) + lab.timestamp = datetime.datetime.now() + lab.put() + + if null_device_count > 0: + host_info.AddNullDevices(host.hostname, null_device_count) return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) -- cgit v1.2.3 From 870d2d578a68d2f9eaca61d531f717c8a330ac97 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 15 May 2018 18:55:12 +0900 Subject: Cover Q and O-MR1 variations and remove codename. Test: dev server Bug: 79726659 Change-Id: I63a8693be39d3d9afcd0e39cb503fa2a9038af04 --- gae/webapp/src/dashboard/build_list.py | 29 ++++++++++++++++++-------- gae/webapp/src/scheduler/schedule_worker.py | 13 ++++++++---- gae/webapp/src/tasks/indexing.py | 17 ++++++++------- gae/webapp/static/build.html | 6 ++++-- gae/webapp/static/create_job_template.html | 32 ++++++++++++++--------------- gae/webapp/static/job.html | 12 +---------- gae/webapp/static/schedule.html | 12 +---------- 7 files changed, 59 insertions(+), 62 deletions(-) diff --git a/gae/webapp/src/dashboard/build_list.py b/gae/webapp/src/dashboard/build_list.py index 491f10a..f0475a8 100644 --- a/gae/webapp/src/dashboard/build_list.py +++ b/gae/webapp/src/dashboard/build_list.py @@ -16,6 +16,7 @@ # import datetime +import re from webapp.src.handlers import base from webapp.src.proto import model @@ -41,16 +42,26 @@ def ReadBuildInfo(target_branch=""): device_builds = {} gsi_builds = {} + gcs_pattern = "^gs://.*" + q_pattern = "(git_)?(aosp-)?q.*" + p_pattern = "(git_)?(aosp-)?p.*" + o_mr1_pattern = "(git_)?(aosp-)?o[^-]*-m.*" + o_pattern = "(git_)?(aosp-)?o.*" + if builds: for build in builds: - if build.manifest_branch.startswith("git_oc-mr1"): + if re.match(gcs_pattern, build.manifest_branch): + m_branch = "GCS" + elif re.match(q_pattern, build.manifest_branch): + m_branch = "Q" + elif re.match(p_pattern, build.manifest_branch): + m_branch = "P" + elif re.match(o_mr1_pattern, build.manifest_branch): m_branch = "O-MR1" - elif build.manifest_branch.startswith("git_oc-"): + elif re.match(o_pattern, build.manifest_branch): m_branch = "O" - elif build.manifest_branch.startswith("gcs"): - m_branch = "GCS" else: - m_branch = "P" + m_branch = "Unknown" if target_branch and target_branch != m_branch: continue @@ -65,14 +76,14 @@ def ReadBuildInfo(target_branch=""): test_builds[m_branch] = [build] elif build.artifact_type == "device": if m_branch in device_builds: - device_builds[m_branch].append(build) + device_builds[m_branch].append(build) else: - device_builds[m_branch] = [build] + device_builds[m_branch] = [build] elif build.artifact_type == "gsi": if m_branch in gsi_builds: - gsi_builds[m_branch].append(build) + gsi_builds[m_branch].append(build) else: - gsi_builds[m_branch] = [build] + gsi_builds[m_branch] = [build] else: print("unknown artifact_type %s" % build.artifact_type) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index c2192e9..9b1ffc0 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -57,14 +57,17 @@ def GetTestVersionType(manifest_branch, gsi_branch, test_type=0): return value | Status.TEST_TYPE_DICT[Status.TEST_TYPE_TOT] gcs_pattern = "^gs://.*/v([0-9.]*)/.*" - p_pattern = "(git_)?p.*" - o_mr1_pattern = "(git_)?o.*mr1.*" - o_pattern = "(git_)?o.*" - master_pattern = "(git_)master" + q_pattern = "(git_)?(aosp-)?q.*" + p_pattern = "(git_)?(aosp-)?p.*" + o_mr1_pattern = "(git_)?(aosp-)?o[^-]*-m.*" + o_pattern = "(git_)?(aosp-)?o.*" + master_pattern = "(git_)?(aosp-)?master" gcs_search = re.search(gcs_pattern, manifest_branch) if gcs_search: device_version = gcs_search.group(1) + elif re.match(q_pattern, manifest_branch): + device_version = "10.0" elif re.match(p_pattern, manifest_branch): device_version = "9.0" elif re.match(o_mr1_pattern, manifest_branch): @@ -80,6 +83,8 @@ def GetTestVersionType(manifest_branch, gsi_branch, test_type=0): gcs_search = re.search(gcs_pattern, gsi_branch) if gcs_search: gsi_version = gcs_search.group(1) + elif re.match(q_pattern, gsi_branch): + gsi_version = "10.0" elif re.match(p_pattern, gsi_branch): gsi_version = "9.0" elif re.match(o_mr1_pattern, gsi_branch): diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index a86e839..1a637c3 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -97,15 +97,14 @@ class IndexingHandler(webapp2.RequestHandler): elif model_type == "lab": pass elif model_type == "job": - if not entity.test_type: - # uses bits 0-1 to indicate version. - test_type = schedule_worker.GetTestVersionType( - entity.manifest_branch, entity.gsi_branch) - # uses bit 2 - if entity.require_signed_device_build: - test_type |= ( - Status.TEST_TYPE_DICT[Status.TEST_TYPE_SIGNED]) - entity.test_type = test_type + # uses bits 0-1 to indicate version. + test_type = schedule_worker.GetTestVersionType( + entity.manifest_branch, entity.gsi_branch) + # uses bit 2 + if entity.require_signed_device_build: + test_type |= ( + Status.TEST_TYPE_DICT[Status.TEST_TYPE_SIGNED]) + entity.test_type = test_type if not entity.parent_schedule: # finds and links to a parent schedule. diff --git a/gae/webapp/static/build.html b/gae/webapp/static/build.html index 5358f4b..38b5ff1 100644 --- a/gae/webapp/static/build.html +++ b/gae/webapp/static/build.html @@ -59,9 +59,11 @@

Shortcuts: All, 8.0 (O), - 8.1 (O-MR1), + 8.1 (O-M), 9.0 (P), - GCS

+ 10.0 (Q), + GCS, + Unknown

{% for manifest_branch, builds in all_builds.items() %} diff --git a/gae/webapp/static/create_job_template.html b/gae/webapp/static/create_job_template.html index 2bf23b8..426fa79 100644 --- a/gae/webapp/static/create_job_template.html +++ b/gae/webapp/static/create_job_template.html @@ -67,22 +67,22 @@
Manifest Branch - +
Build Target - +
Build ID - +
PAB account ID - +
@@ -92,22 +92,22 @@
GSI Branch - +
GSI Build ID - +
GSI Build Target - +
GSI PAB account ID - +
@@ -117,22 +117,22 @@
Test Branch
-
+
Test Build ID
-
+
Test Build Target
-
+
Test PAB Account ID
-
+
@@ -168,22 +168,22 @@ Device:
-
+
Hostname:
-
+
Serial:
-
+
Shards:
-
+
diff --git a/gae/webapp/static/job.html b/gae/webapp/static/job.html index ff0228c..a16e3c0 100644 --- a/gae/webapp/static/job.html +++ b/gae/webapp/static/job.html @@ -59,19 +59,9 @@ {% set index = 1 %} @@ -120,6 +121,8 @@ {{ {0: "free", 1: "reserved", 2: "use"}[device.scheduling_status] | default("status key error") }} +
-

Shortcuts: 8.0 (oc) 8.1 (oc-mr1) 9.0 (pi)

+

Shortcuts: 8.0 (O) 8.1 (O-MR1) 9.0 (P)

Create a job

{{ message }}

- - - -
- -
diff --git a/gae/webapp/static/schedule.html b/gae/webapp/static/schedule.html index 2e5069d..c0bde5c 100644 --- a/gae/webapp/static/schedule.html +++ b/gae/webapp/static/schedule.html @@ -56,17 +56,7 @@

Schedule List

-

Shortcuts: 8.0 (oc) 8.1 (oc-mr1) 9.0 (pi)

-
- - -
- -
+

Shortcuts: 8.0 (O) 8.1 (O-MR1) 9.0 (P)


Scheduler Enabled: {{ enabled }} ( Toggle)
-- cgit v1.2.3 From c231deb2c50d8b618656688fba8966f501f7b00c Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 23 May 2018 09:51:34 +0900 Subject: Check an artifact_type to search build duplicates. Test: localhost Bug: 80156095 --- gae/webapp/src/endpoint/build_info.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index c1dfdb7..249e362 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Build Info APIs implemented using Google Cloud Endpoints.""" import datetime @@ -22,9 +21,7 @@ from protorpc import remote from webapp.src.proto import model - -BUILD_INFO_RESOURCE = endpoints.ResourceContainer( - model.BuildInfoMessage) +BUILD_INFO_RESOURCE = endpoints.ResourceContainer(model.BuildInfoMessage) @endpoints.api(name="build_info", version="v1") @@ -42,14 +39,16 @@ class BuildInfoApi(remote.Service): build_query = model.BuildModel.query( model.BuildModel.build_id == request.build_id, model.BuildModel.build_target == request.build_target, - model.BuildModel.build_type == request.build_type - ) + model.BuildModel.build_type == request.build_type, + model.BuildModel.artifact_type == request.artifact_type) existing_builds = build_query.fetch() if existing_builds and len(existing_builds) > 1: - logging.warning("Duplicated builds found for [build_id]{} " - "[build_target]{} [build_type]{}".format( - request.build_id, request.build_target, request.build_type)) + logging.warning( + "Duplicated builds found for [build_id]{} " + "[build_target]{} [build_type]{} [artifact_type]{}".format( + request.build_id, request.build_target, request.build_type, + request.artifact_type)) if request.signed and existing_builds: # only signed builds need to overwrite the exist entities. -- cgit v1.2.3 From 116d1662a92df5f42bf44efa6ce0ba735a6f5be9 Mon Sep 17 00:00:00 2001 From: Keun Soo Yim Date: Wed, 23 May 2018 14:29:02 -0700 Subject: run unit tests at preupload time Test: repo upload Change-Id: Iddfb02441422ab3bf0764c19a4d09bc5bb987799 --- PREUPLOAD.cfg | 9 ++++ gae/script/run-unittest.sh | 17 ------ gae/testing/e2e_test.py | 129 +++++++++++++++++++++++++++++++++++++++++++++ gae/testrunner.py | 128 -------------------------------------------- script/run-unittest.sh | 26 +++++++++ 5 files changed, 164 insertions(+), 145 deletions(-) create mode 100644 PREUPLOAD.cfg delete mode 100755 gae/script/run-unittest.sh create mode 100644 gae/testing/e2e_test.py delete mode 100644 gae/testrunner.py create mode 100755 script/run-unittest.sh diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg new file mode 100644 index 0000000..7049c8c --- /dev/null +++ b/PREUPLOAD.cfg @@ -0,0 +1,9 @@ +[Builtin Hooks] +clang_format = true + +[Builtin Hooks Options] +clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,java + +[Hook Scripts] +test_serving_unittests = ${REPO_ROOT}/test/framework/script/run-unittest.sh + diff --git a/gae/script/run-unittest.sh b/gae/script/run-unittest.sh deleted file mode 100755 index 4e20b0b..0000000 --- a/gae/script/run-unittest.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# -# Copyright 2018 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -python testrunner.py diff --git a/gae/testing/e2e_test.py b/gae/testing/e2e_test.py new file mode 100644 index 0000000..3305862 --- /dev/null +++ b/gae/testing/e2e_test.py @@ -0,0 +1,129 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""App Engine local test runner. + +This program handles properly importing the App Engine SDK so that test modules +can use google.appengine.* APIs and the Google App Engine testbed. + +Example invocation: + + $ python testrunner.py [--sdk-path ~/google-cloud-sdk] +""" + +import argparse +import os +import subprocess +import sys +import unittest + + +def ExecuteOneShellCommand(cmd): + """Executes one shell command and returns (stdout, stderr, exit_code). + + Args: + cmd: string, a shell command. + + Returns: + tuple(string, string, int), containing stdout, stderr, exit_code of + the shell command. + """ + p = subprocess.Popen( + str(cmd), shell=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + return (stdout, stderr, p.returncode) + + +def fixup_paths(path): + """Adds GAE SDK path to system path and appends it to the google path + if that already exists.""" + # Not all Google packages are inside namespace packages, which means + # there might be another non-namespace package named `google` already on + # the path and simply appending the App Engine SDK to the path will not + # work since the other package will get discovered and used first. + # This emulates namespace packages by first searching if a `google` package + # exists by importing it, and if so appending to its module search path. + try: + import google + google.__path__.append("{0}/google".format(path)) + except ImportError: + pass + + sys.path.insert(0, path) + + +def main(sdk_path, test_path, test_pattern): + + if not sdk_path: + # Get sdk path by running gcloud command. + stdout, stderr, _ = ExecuteOneShellCommand( + "gcloud info --format='value(installation.sdk_root)'") + + if stderr: + print("Cannot find google cloud sdk path.") + return 1 + sdk_path = str.strip(stdout) + + # If the SDK path points to a Google Cloud SDK installation + # then we should alter it to point to the GAE platform location. + if os.path.exists(os.path.join(sdk_path, 'platform/google_appengine')): + sdk_path = os.path.join(sdk_path, 'platform/google_appengine') + + # Make sure google.appengine.* modules are importable. + fixup_paths(sdk_path) + + # Make sure all bundled third-party packages are available. + import dev_appserver + dev_appserver.fix_sys_path() + + # Loading appengine_config from the current project ensures that any + # changes to configuration there are available to all tests (e.g. + # sys.path modifications, namespaces, etc.) + try: + import appengine_config + (appengine_config) + except ImportError: + print('Note: unable to import appengine_config.') + + # Discover and run tests. + suite = unittest.loader.TestLoader().discover(test_path, test_pattern) + print('Suite', suite) + return unittest.TextTestRunner(verbosity=2).run(suite) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + '--sdk_path', + help='The path to the Google App Engine SDK or the Google Cloud SDK.', + default=None) + parser.add_argument( + '--test-path', + help='The path to look for tests, defaults to the current directory.', + default=os.getcwd()) + parser.add_argument( + '--test-pattern', + help='The file pattern for test modules, defaults to *_test.py.', + default='*_test.py') + + args = parser.parse_args() + + result = main(args.sdk_path, args.test_path, args.test_pattern) + + if not result.wasSuccessful(): + sys.exit(1) diff --git a/gae/testrunner.py b/gae/testrunner.py deleted file mode 100644 index 8294f44..0000000 --- a/gae/testrunner.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (C) 2018 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -"""App Engine local test runner. - -This program handles properly importing the App Engine SDK so that test modules -can use google.appengine.* APIs and the Google App Engine testbed. - -Example invocation: - - $ python testrunner.py [--sdk-path ~/google-cloud-sdk] -""" - -import argparse -import os -import subprocess -import sys -import unittest - - -def ExecuteOneShellCommand(cmd): - """Executes one shell command and returns (stdout, stderr, exit_code). - - Args: - cmd: string, a shell command. - - Returns: - tuple(string, string, int), containing stdout, stderr, exit_code of - the shell command. - """ - p = subprocess.Popen( - str(cmd), shell=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = p.communicate() - return (stdout, stderr, p.returncode) - - -def fixup_paths(path): - """Adds GAE SDK path to system path and appends it to the google path - if that already exists.""" - # Not all Google packages are inside namespace packages, which means - # there might be another non-namespace package named `google` already on - # the path and simply appending the App Engine SDK to the path will not - # work since the other package will get discovered and used first. - # This emulates namespace packages by first searching if a `google` package - # exists by importing it, and if so appending to its module search path. - try: - import google - google.__path__.append("{0}/google".format(path)) - except ImportError: - pass - - sys.path.insert(0, path) - - -def main(sdk_path, test_path, test_pattern): - - if not sdk_path: - # Get sdk path by running gcloud command. - stdout, stderr, _ = ExecuteOneShellCommand( - "gcloud info --format='value(installation.sdk_root)'") - - if stderr: - print("Cannot find google cloud sdk path.") - return 1 - sdk_path = str.strip(stdout) - - # If the SDK path points to a Google Cloud SDK installation - # then we should alter it to point to the GAE platform location. - if os.path.exists(os.path.join(sdk_path, 'platform/google_appengine')): - sdk_path = os.path.join(sdk_path, 'platform/google_appengine') - - # Make sure google.appengine.* modules are importable. - fixup_paths(sdk_path) - - # Make sure all bundled third-party packages are available. - import dev_appserver - dev_appserver.fix_sys_path() - - # Loading appengine_config from the current project ensures that any - # changes to configuration there are available to all tests (e.g. - # sys.path modifications, namespaces, etc.) - try: - import appengine_config - (appengine_config) - except ImportError: - print('Note: unable to import appengine_config.') - - # Discover and run tests. - suite = unittest.loader.TestLoader().discover(test_path, test_pattern) - return unittest.TextTestRunner(verbosity=2).run(suite) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument( - '--sdk_path', - help='The path to the Google App Engine SDK or the Google Cloud SDK.', - default=None) - parser.add_argument( - '--test-path', - help='The path to look for tests, defaults to the current directory.', - default=os.getcwd()) - parser.add_argument( - '--test-pattern', - help='The file pattern for test modules, defaults to *_test.py.', - default='*_test.py') - - args = parser.parse_args() - - result = main(args.sdk_path, args.test_path, args.test_pattern) - - if not result.wasSuccessful(): - sys.exit(1) diff --git a/script/run-unittest.sh b/script/run-unittest.sh new file mode 100755 index 0000000..6ce6949 --- /dev/null +++ b/script/run-unittest.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# Copyright 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z "$ANDROID_BUILD_TOP" ]; then + echo "Missing ANDROID_BUILD_TOP env variable. Run 'lunch' first." + exit 1 +fi + +# Runs all unit tests under test/vti/test_serving/gae using an e2e_test framework. +pushd $ANDROID_BUILD_TOP/test/vti/test_serving/gae +python testing/e2e_test.py +popd + -- cgit v1.2.3 From a00dd18a63f33e00e30697eeaaccc2d5dd2bac8c Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 15 May 2018 10:48:43 +0900 Subject: Fix unit tests Test: ./script/run-unittest.sh Bug: 77617865 --- gae/webapp/src/scheduler/job_heartbeat_test.py | 4 +- gae/webapp/src/scheduler/schedule_worker_test.py | 52 +++++++++++++++++++----- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/gae/webapp/src/scheduler/job_heartbeat_test.py b/gae/webapp/src/scheduler/job_heartbeat_test.py index ea65906..20b5bc6 100644 --- a/gae/webapp/src/scheduler/job_heartbeat_test.py +++ b/gae/webapp/src/scheduler/job_heartbeat_test.py @@ -76,7 +76,7 @@ class JobHeartbeatTest(unittest.TestCase): gsi_pab_account_id = "1234567890" test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] test_branch = "gsi_branch" - test_build_target = "gsi_build_target-user" + test_build_target = "test_build_target-user" test_pab_account_id = "1234567890" lab_name = "test_lab" @@ -165,7 +165,7 @@ class JobHeartbeatTest(unittest.TestCase): scheduler.response = mock.Mock() scheduler.response.write = mock.Mock() - print("Creating jobs...") + print("\nCreating jobs...") scheduler.post() jobs = model.JobModel.query().fetch() self.assertEqual(2, len(jobs)) diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index 418b9ad..cafb68d 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -61,18 +61,30 @@ class ScheduleHandlerTest(unittest.TestCase): This test defines that each model only has a single entity, and asserts that a job is created. """ + + manifest_branch = "manifest_branch" + build_target = "device_build_target-user" + gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + gsi_branch = "gsi_branch" + gsi_build_target = "gsi_build_target-user" + test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + test_branch = "gsi_branch" + test_build_target = "test_build_target-user" + schedule = model.ScheduleModel() schedule.schedule_type = "test" schedule.test_name = "vts/vts" - schedule.manifest_branch = "branch1" - schedule.build_target = "product1-type1" + schedule.manifest_branch = manifest_branch + schedule.build_target = build_target schedule.device = ["test_lab1/product1"] schedule.shards = 1 schedule.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - schedule.gsi_branch = "gsi_branch" - schedule.gsi_build_target = "gsi_build_target" - schedule.test_branch = "test_branch" - schedule.test_build_target = "test_build_target" + schedule.gsi_storage_type = gsi_storage_type + schedule.gsi_branch = gsi_branch + schedule.gsi_build_target = gsi_build_target + schedule.test_storage_type = test_storage_type + schedule.test_branch = test_branch + schedule.test_build_target = test_build_target schedule.require_signed_device_build = False schedule.put() @@ -90,11 +102,31 @@ class ScheduleHandlerTest(unittest.TestCase): device.hostname = "test_lab1_host1" device.put() + # create a device build + build = model.BuildModel() + build.manifest_branch = manifest_branch + build.build_id = "1000000" + build.build_target = "device_build_target" + build.build_type = "user" + build.artifact_type = "device" + build.put() + + # create a gsi build + build = model.BuildModel() + build.manifest_branch = gsi_branch + build.build_id = "2000000" + build.build_target = "gsi_build_target" + build.build_type = "user" + build.artifact_type = "gsi" + build.put() + + # create a test build build = model.BuildModel() - build.manifest_branch = "branch1" - build.build_id = "0000000" - build.build_target = "product1" - build.build_type = "type1" + build.manifest_branch = gsi_branch + build.build_id = "3000000" + build.build_target = "test_build_target" + build.build_type = "user" + build.artifact_type = "test" build.put() self.scheduler.post() -- cgit v1.2.3 From 14ce1d4e0d128999fbb4ceeee4859d03ace6af27 Mon Sep 17 00:00:00 2001 From: Hyunwoo Ko Date: Fri, 25 May 2018 15:55:51 +0900 Subject: Show utilization info of each product. Test: deploy to dev.appspot Bug: 80280033 Change-Id: I56d57cdbb778c4539421956ae9d4f22ca75edc9a --- gae/webapp/src/dashboard/device_list.py | 29 ++------- gae/webapp/src/dashboard/device_stats.py | 107 +++++++++++++++++++++++++++++++ gae/webapp/static/device.html | 27 ++++++++ 3 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 gae/webapp/src/dashboard/device_stats.py diff --git a/gae/webapp/src/dashboard/device_list.py b/gae/webapp/src/dashboard/device_list.py index 4b18cf2..371f858 100644 --- a/gae/webapp/src/dashboard/device_list.py +++ b/gae/webapp/src/dashboard/device_list.py @@ -18,20 +18,7 @@ from webapp.src.handlers import base from webapp.src.proto import model from webapp.src import vtslab_status - - -class DeviceStats(object): - """Device stats class. - - Attributes: - total: int, total device count. - utilization: int, the device utilization ratio (%). - error_ratio: int, the device error ratio (%). - """ - - total = 0 - utilization = -1 - error_ratio = -1 +from webapp.src.dashboard import device_stats class DevicePage(base.BaseHandler): @@ -47,27 +34,23 @@ class DevicePage(base.BaseHandler): lab_query = model.LabModel.query() labs = lab_query.fetch() - stats = DeviceStats() + stats = device_stats.DeviceStats() if devices: devices = sorted( devices, key=lambda x: (x.hostname, x.product, x.status), reverse=False) - count_active, count_idle, count_error = 0, 0, 0 for device in devices: + device_product_lowcase = device.product.lower() if device.scheduling_status == vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["free"]: if (device.status == vtslab_status.DEVICE_STATUS_DICT["error"] or device.status == vtslab_status.DEVICE_STATUS_DICT["no-response"]): - count_error += 1 + stats.add_error(device_product_lowcase) else: # it shouldn't be in use state. - count_idle += 1 + stats.add_idle(device_product_lowcase) else: # includes both use and reserved - count_active += 1 - - stats.total = count_active + count_idle + count_error - stats.utilization = count_active * 100 / stats.total - stats.error_ratio = count_error * 100 / stats.total + stats.add_active(device_product_lowcase) template_values = { "devices": devices, diff --git a/gae/webapp/src/dashboard/device_stats.py b/gae/webapp/src/dashboard/device_stats.py new file mode 100644 index 0000000..9fbf64f --- /dev/null +++ b/gae/webapp/src/dashboard/device_stats.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +class DeviceStats(object): + """Device stats class. + + Attributes: + _total: int, total device count. + _active: int, device count in active state. + _error: int, device count in error state. + _idle: int, device count in ready state. + """ + + def __init__(self, parent=True): + self._total = 0 + self._active = 0 + self._error = 0 + self._idle = 0 + if parent == True: + self._product_stat = {} + + @property + def total(self): + return self._total + + @total.setter + def total(self, total): + self._total = total + + @property + def active(self): + return self._active + + @active.setter + def active(self, active): + self._active = active + + @property + def error(self): + return self._error + + @error.setter + def error(self, error): + self._error = error + + @property + def idle(self): + return self._idle + + @idle.setter + def idle(self, idle): + self._idle = idle + + @property + def utilization(self): + return self._active * 100 / self._total + + @property + def error_ratio(self): + return self._error * 100 / self._total + + @property + def product_stat(self): + return self._product_stat + + def __getitem__(self, product): + return self._product_stat[product] + + def _add_total(self, product=""): + self._total += 1 + if product: + if product not in self._product_stat: + self._product_stat[product] = DeviceStats(False) + self._product_stat[product].total += 1 + + def add_active(self, product=""): + self._add_total(product) + self._active += 1 + if product: + self._product_stat[product].active += 1 + + def add_error(self, product=""): + self._add_total(product) + self._error += 1 + if product: + self._product_stat[product].error += 1 + + def add_idle(self, product=""): + self._add_total(product) + self._idle += 1 + if product: + self._product_stat[product].idle += 1 \ No newline at end of file diff --git a/gae/webapp/static/device.html b/gae/webapp/static/device.html index 2a8db2f..1d2f676 100644 --- a/gae/webapp/static/device.html +++ b/gae/webapp/static/device.html @@ -57,8 +57,35 @@

Device List

+

Device Utilization

+
+ + + {% set index = 1 %} + {% for product in stats.product_stat %} + + + {% endfor %} +
# + Product + Utilization + Error Ratio + Device Count +
+ {{ index }} + {% set index = index + 1 %} + + {{product}} + + {{stats[product].utilization}}% + + {{stats[product].error_ratio}}% + + {{stats[product].total}} +
Utilization: {{ stats.utilization }}%, Error Ratio: {{ stats.error_ratio }}%, Total Device Count: {{ stats.total}} (Now: {{ convert_time(now).strftime("%Y-%m-%d %H:%M:%S %Z") }})
+

Device List

{% set index = 1 %} {% for schedule in schedules %} @@ -110,6 +111,10 @@ {% else %} error(s): {{ schedule.error_count }} {% endif %} + {% endfor %}
# -- cgit v1.2.3 From a1204898613a4929f66c3903935c12c9d574a51b Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 30 May 2018 10:51:26 +0900 Subject: Update ndb models for host and device equipment. Test: mma Bug: 79541234 Change-Id: Iee00e4dbaf64c7512706c4bc6fd05cb538feafde --- gae/webapp/src/proto/model.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 324264f..3efaf0e 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -91,6 +91,9 @@ class ScheduleModel(ndb.Model): suspended = ndb.BooleanProperty() image_package_repo_base = ndb.StringProperty() + required_host_equipment = ndb.StringProperty(repeated=True) + required_device_equipment = ndb.StringProperty(repeated=True) + class ScheduleControlInfoMessage(messages.Message): """A message for representing a schedule control data entry.""" @@ -100,7 +103,7 @@ class ScheduleControlInfoMessage(messages.Message): class ScheduleInfoMessage(messages.Message): """A message for representing an individual schedule entry.""" - # Next ID = 25 + # Next ID = 27 # schedule name for green build schedule, optional. name = messages.StringField(16) schedule_type = messages.StringField(19) @@ -134,6 +137,9 @@ class ScheduleInfoMessage(messages.Message): param = messages.StringField(8, repeated=True) retry_count = messages.IntegerField(15) + required_host_equipment = messages.StringField(25, repeated=True) + required_device_equipment = messages.StringField(26, repeated=True) + class LabModel(ndb.Model): """A model for representing an individual lab entry.""" @@ -146,12 +152,14 @@ class LabModel(ndb.Model): devices = ndb.StringProperty() timestamp = ndb.DateTimeProperty(auto_now=False) vtslab_version = ndb.StringProperty() + host_equipment = ndb.StringProperty(repeated=True) class LabDeviceInfoMessage(messages.Message): """A message for representing an individual lab host's device entry.""" serial = messages.StringField(1, repeated=False) product = messages.StringField(2, repeated=False) + device_equipment = messages.StringField(3, repeated=True) class LabHostInfoMessage(messages.Message): @@ -161,6 +169,7 @@ class LabHostInfoMessage(messages.Message): script = messages.StringField(3) device = messages.MessageField(LabDeviceInfoMessage, 4, repeated=True) vtslab_version = messages.StringField(5) + host_equipment = messages.StringField(6, repeated=True) class LabInfoMessage(messages.Message): @@ -179,6 +188,7 @@ class DeviceModel(ndb.Model): status = ndb.IntegerProperty() scheduling_status = ndb.IntegerProperty() timestamp = ndb.DateTimeProperty(auto_now=False) + device_equipment = ndb.StringProperty(repeated=True) class DeviceInfoMessage(messages.Message): -- cgit v1.2.3 From 881145ce0ec8cdfce5f864add443b1b9fb3b6987 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Thu, 31 May 2018 13:11:44 +0900 Subject: Look up required equipment when creating a job. Test: mma Bug: 79541234 --- gae/webapp/src/scheduler/schedule_worker.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 9b1ffc0..cc9eedd 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -271,9 +271,10 @@ class ScheduleHandler(webapp2.RequestHandler): "Unexpected storage type (%s)." % storage_type) setattr(new_job, build_id_text, build_id) - if ((not new_job.manifest_branch or new_job.build_id) and - (not new_job.gsi_branch or new_job.gsi_build_id) and - (not new_job.test_branch or new_job.test_build_id)): + if ((not new_job.manifest_branch or new_job.build_id) + and (not new_job.gsi_branch or new_job.gsi_build_id) + and + (not new_job.test_branch or new_job.test_build_id)): new_job.build_id = new_job.build_id.replace("gcs", "") new_job.gsi_build_id = (new_job.gsi_build_id.replace( "gcs", "")) @@ -396,6 +397,9 @@ class ScheduleHandler(webapp2.RequestHandler): available_devices = {} if target_labs: for lab in target_labs: + if not (set(schedule.required_host_equipment) <= set( + lab.host_equipment)): + continue self.logger.Println("- Host: %s" % lab.hostname) self.logger.Indent() device_query = model.DeviceModel.query( @@ -410,7 +414,9 @@ class ScheduleHandler(webapp2.RequestHandler): ]) and (device.scheduling_status == Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) and device.product.lower() == - target_product_type.lower()): + target_product_type.lower() + and (set(schedule.required_device_equipment) <= + set(device.device_equipment))): self.logger.Println("- Found %s %s %s" % (device.product, device.status, device.serial)) -- cgit v1.2.3 From 1a484639e336f06877189e3b4343478dbc1a1daf Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 30 May 2018 11:10:50 +0900 Subject: Update devices with lab info message. Test: mma Bug: 79541234 Change-Id: Ic12c252ff7086d6fb5579734ffa8cd02f6559437 --- gae/webapp/src/endpoint/lab_info.py | 69 +++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index da590a0..5159a45 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -16,19 +16,18 @@ import datetime import endpoints +import logging from protorpc import remote from google.appengine.ext import ndb +from webapp.src import vtslab_status as Status from webapp.src.endpoint import host_info from webapp.src.proto import model - -LAB_INFO_RESOURCE = endpoints.ResourceContainer( - model.LabInfoMessage) -LAB_HOST_INFO_RESOURCE = endpoints.ResourceContainer( - model.LabHostInfoMessage) +LAB_INFO_RESOURCE = endpoints.ResourceContainer(model.LabInfoMessage) +LAB_HOST_INFO_RESOURCE = endpoints.ResourceContainer(model.LabHostInfoMessage) @endpoints.api(name='lab_info', version='v1') @@ -63,8 +62,7 @@ class LabInfoApi(remote.Service): duplicate_query = model.LabModel.query( model.LabModel.name == request.name, model.LabModel.owner == request.owner, - model.LabModel.hostname == host.hostname - ) + model.LabModel.hostname == host.hostname) duplicates = duplicate_query.fetch() if duplicates: lab = duplicates[0] @@ -76,15 +74,52 @@ class LabInfoApi(remote.Service): lab.hostname = host.hostname lab.ip = host.ip lab.script = host.script - devices = [] + null_device_count = 0 - if host.device: - for device in host.device: - devices.append("%s=%s" % (device.serial, device.product)) - if device.product == "null": - null_device_count += 1 - if devices: - lab.devices = ",".join(devices) + devices_to_put = [] + for config_device in host.device: + if config_device.product == "null": + null_device_count += 1 + continue + if config_device.serial and config_device.product: + device_query = model.DeviceModel.query( + model.DeviceModel.serial == config_device.serial, + model.DeviceModel.product == config_device.product) + devices = device_query.fetch() + if devices: + device = devices[0] + if (device.hostname != host.hostname) and ( + device.status != + Status.DEVICE_STATUS_DICT["no-response"]): + logging.error( + "{} is alive in another host.".format( + config_device.serial)) + # TODO: send an alert to lab.admin + continue + if device.hostname == host.hostname and set( + device.device_equipment) == set( + config_device.device_equipment): + # no need to update. + continue + else: + device = model.DeviceModel() + device.status = Status.DEVICE_STATUS_DICT[ + "no-response"] + device.product = config_device.product + device.serial = config_device.serial + device.hostname = host.hostname + device.scheduling_status = ( + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) + device.timestamp = datetime.datetime.now() + device.device_equipment = config_device.device_equipment + devices_to_put.append(device) + else: + logging.error("Lab config does not have device " + "information correctly; it should " + "specify device product and serial.") + if devices_to_put: + ndb.put_multi(devices_to_put) + lab.timestamp = datetime.datetime.now() lab.put() @@ -103,8 +138,7 @@ class LabInfoApi(remote.Service): def set_version(self, request): """Sets vtslab version of the host """ lab_query = model.LabModel.query( - model.LabModel.hostname == request.hostname - ) + model.LabModel.hostname == request.hostname) labs = lab_query.fetch() for lab in labs: @@ -113,4 +147,3 @@ class LabInfoApi(remote.Service): return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) - -- cgit v1.2.3 From 0f7476fcc1867e82604ea008d76365ded4282ded Mon Sep 17 00:00:00 2001 From: Jongmok Date: Fri, 18 May 2018 15:52:11 +0900 Subject: Create a job from schedules Test: localhost Bug: 79956700 Change-Id: Ie9fca547d5c1e31dcf0433f2297d5aa9b83c270b --- gae/webapp/src/dashboard/schedule_list.py | 12 ++++++++++++ gae/webapp/src/scheduler/schedule_worker.py | 20 ++++++++++++++++---- gae/webapp/static/schedule.html | 5 +++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/gae/webapp/src/dashboard/schedule_list.py b/gae/webapp/src/dashboard/schedule_list.py index 830ebe1..50de2ad 100644 --- a/gae/webapp/src/dashboard/schedule_list.py +++ b/gae/webapp/src/dashboard/schedule_list.py @@ -15,6 +15,7 @@ # limitations under the License. # +from google.appengine.api import taskqueue from google.appengine.ext import ndb from webapp.src.handlers import base @@ -45,6 +46,17 @@ class SchedulePage(base.BaseHandler): schedule.put() email_util.send_schedule_suspension_notification(schedule) + create_job_key = self.request.get("create_job") + if create_job_key: + taskqueue.add( + url="/worker/schedule_handler", + target="worker", + queue_name="queue-schedule", + transactional=False, + params={ + "schedule_key": create_job_key + }) + toggle = self.request.get("schedule_enable_status_toggle", default_value="0") schedule_control = model.ScheduleControlModel.query() diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 9b1ffc0..5f7bb72 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -19,6 +19,8 @@ import datetime import logging import re +from google.appengine.ext import ndb + from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.utils import logger @@ -169,9 +171,16 @@ class ScheduleHandler(webapp2.RequestHandler): def post(self): self.logger.Clear() - schedule_query = model.ScheduleModel.query( - model.ScheduleModel.suspended != True) - schedules = schedule_query.fetch() + manual_job = False + schedule_key = self.request.get("schedule_key") + if schedule_key: + key = ndb.key.Key(urlsafe=schedule_key) + manual_job = True + schedules = [key.get()] + else: + schedule_query = model.ScheduleModel.query( + model.ScheduleModel.suspended != True) + schedules = schedule_query.fetch() if schedules: for schedule in schedules: @@ -182,7 +191,7 @@ class ScheduleHandler(webapp2.RequestHandler): self.logger.Println("Build Target: %s" % schedule.build_target) self.logger.Println("Device: %s" % schedule.device) self.logger.Indent() - if not self.NewPeriod(schedule): + if not manual_job and not self.NewPeriod(schedule): self.logger.Println("- Skipped") self.logger.Unindent() continue @@ -236,6 +245,9 @@ class ScheduleHandler(webapp2.RequestHandler): test_type |= Status.TEST_TYPE_DICT[Status.TEST_TYPE_SIGNED] new_job.test_type = test_type + if manual_job: + test_type |= Status.TEST_TYPE_DICT[Status.TEST_TYPE_MANUAL] + new_job.build_id = "" new_job.gsi_build_id = "" new_job.test_build_id = "" diff --git a/gae/webapp/static/schedule.html b/gae/webapp/static/schedule.html index c0bde5c..791c5c1 100644 --- a/gae/webapp/static/schedule.html +++ b/gae/webapp/static/schedule.html @@ -73,6 +73,7 @@ priority timestamp resume/suspend + create a job
+ {% if not schedule.suspended %} + + {% endif %}
-- cgit v1.2.3 From 345a452bde84585218855ffa814881ba10190b11 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Wed, 30 May 2018 06:52:17 +0900 Subject: Create a unittest base. Test: python testing/e2e_test.py Bug: 77617865 Change-Id: Ic7ed68aef12682b45d580ccaea97b0437bfdec72 --- gae/webapp/src/scheduler/job_heartbeat_test.py | 133 ++--------- gae/webapp/src/scheduler/schedule_worker_test.py | 93 ++------ gae/webapp/src/tasks/indexing_test.py | 98 +------- gae/webapp/src/testing/__init__.py | 0 gae/webapp/src/testing/unittest_base.py | 287 +++++++++++++++++++++++ gae/webapp/src/utils/model_util_test.py | 128 +--------- 6 files changed, 345 insertions(+), 394 deletions(-) create mode 100644 gae/webapp/src/testing/__init__.py create mode 100644 gae/webapp/src/testing/unittest_base.py diff --git a/gae/webapp/src/scheduler/job_heartbeat_test.py b/gae/webapp/src/scheduler/job_heartbeat_test.py index 20b5bc6..01cf12c 100644 --- a/gae/webapp/src/scheduler/job_heartbeat_test.py +++ b/gae/webapp/src/scheduler/job_heartbeat_test.py @@ -27,12 +27,10 @@ from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.scheduler import job_heartbeat from webapp.src.scheduler import schedule_worker +from webapp.src.testing import unittest_base -from google.appengine.ext import ndb -from google.appengine.ext import testbed - -class JobHeartbeatTest(unittest.TestCase): +class JobHeartbeatTest(unittest_base.UnitTestBase): """Tests for PeriodicJobHeartBeat cron class. Attributes: @@ -42,128 +40,45 @@ class JobHeartbeatTest(unittest.TestCase): def setUp(self): """Initializes test""" - # Create the Testbed class instance and initialize service stubs. - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.setup_env(app_id="vtslab-schedule-unittest") - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - self.testbed.init_mail_stub() - # Clear cache between tests. - ndb.get_context().clear_cache() + super(JobHeartbeatTest, self).setUp() # Mocking PeriodicJobHeartBeat and essential methods. self.job_heartbeat = job_heartbeat.PeriodicJobHeartBeat(mock.Mock()) self.job_heartbeat.response = mock.Mock() self.job_heartbeat.response.write = mock.Mock() - def tearDown(self): - self.testbed.deactivate() - def testJobHearbeat(self): - """""" - # schedule information - priority = "top" - period = 360 - build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - manifest_branch = "manifest_branch" - build_target = "device_build_target-user" - pab_account_id = "1234567890" + """Asserts job heartbeat detects unavailable jobs.""" + num_of_devices = 2 shards = 2 - retry_count = 1 - gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - gsi_branch = "gsi_branch" - gsi_build_target = "gsi_build_target-user" - gsi_pab_account_id = "1234567890" - test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - test_branch = "gsi_branch" - test_build_target = "test_build_target-user" - test_pab_account_id = "1234567890" - - lab_name = "test_lab" - host_name = "test_host" - devices_list = ["device1", "device2"] - - # create a device build - build = model.BuildModel() - build.manifest_branch = manifest_branch - build.build_id = "1000000" - build.build_target = "device_build_target" - build.build_type = "user" - build.artifact_type = "device" - build.timestamp = datetime.datetime.now() - build.signed = False - build.put() - - # create a gsi build - build = model.BuildModel() - build.manifest_branch = gsi_branch - build.build_id = "2000000" - build.build_target = "gsi_build_target" - build.build_type = "user" - build.artifact_type = "gsi" - build.timestamp = datetime.datetime.now() - build.signed = False - build.put() - - # create a test build - build = model.BuildModel() - build.manifest_branch = gsi_branch - build.build_id = "3000000" - build.build_target = "test_build_target" - build.build_type = "user" - build.artifact_type = "test" - build.timestamp = datetime.datetime.now() - build.signed = False - build.put() - - # create a lab - lab = model.LabModel() - lab.name = lab_name - lab.hostname = host_name - lab.owner = "test@google.com" + + lab = self.GenerateLabModel() lab.put() - # create devices - for dev in devices_list: - for num in xrange(shards): - device = model.DeviceModel() - device.hostname = host_name - device.product = dev - device.serial = "{}{}".format(dev, num) - device.status = Status.DEVICE_STATUS_DICT["fastboot"] - device.scheduling_status = ( - Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) - device.timestamp = datetime.datetime.now() + devices = [] + for _ in range(num_of_devices): + for i in range(shards): + device = self.GenerateDeviceModel( + hostname=lab.hostname, product="product{}".format(i)) device.put() + devices.append(device) - # create schedules - for device in devices_list: - schedule = model.ScheduleModel() - schedule.priority = priority - schedule.test_name = "test/{}".format(device) - schedule.period = period - schedule.build_storage_type = build_storage_type - schedule.manifest_branch = manifest_branch - schedule.build_target = build_target - schedule.device_pab_account_id = pab_account_id - schedule.shards = shards - schedule.retry_count = retry_count - schedule.gsi_storage_type = gsi_storage_type - schedule.gsi_branch = gsi_branch - schedule.gsi_build_target = gsi_build_target - schedule.gsi_pab_account_id = gsi_pab_account_id - schedule.test_storage_type = test_storage_type - schedule.test_branch = test_branch - schedule.test_build_target = test_build_target - schedule.test_pab_account_id = test_pab_account_id - schedule.device = [] - schedule.device.append("{}/{}".format(lab_name, device)) + schedules = [] + for device in devices: + schedule = self.GenerateScheduleModel( + lab_model=lab, device_model=device, shards=shards) schedule.put() + schedules.append(schedule) + + for schedule in schedules: + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() # Mocking ScheduleHandler and essential methods. scheduler = schedule_worker.ScheduleHandler(mock.Mock()) scheduler.response = mock.Mock() scheduler.response.write = mock.Mock() + scheduler.request.get = mock.MagicMock(return_value="") print("\nCreating jobs...") scheduler.post() diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index cafb68d..1e07417 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -25,35 +25,24 @@ except ImportError: from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.scheduler import schedule_worker +from webapp.src.testing import unittest_base -from google.appengine.ext import ndb -from google.appengine.ext import testbed - -class ScheduleHandlerTest(unittest.TestCase): +class ScheduleHandlerTest(unittest_base.UnitTestBase): """Tests for ScheduleHandler. Attributes: - testbed: A Testbed instance which provides local unit testing. scheduler: A mock schedule_worker.ScheduleHandler. """ def setUp(self): """Initializes test""" - # Create the Testbed class instance and initialize service stubs. - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - # Clear cache between tests. - ndb.get_context().clear_cache() + super(ScheduleHandlerTest, self).setUp() # Mocking ScheduleHandler and essential methods. self.scheduler = schedule_worker.ScheduleHandler(mock.Mock()) self.scheduler.response = mock.Mock() self.scheduler.response.write = mock.Mock() - - def tearDown(self): - self.testbed.deactivate() + self.scheduler.request.get = mock.MagicMock(return_value="") def testSimpleJobCreation(self): """Asserts a job is created. @@ -61,80 +50,26 @@ class ScheduleHandlerTest(unittest.TestCase): This test defines that each model only has a single entity, and asserts that a job is created. """ - - manifest_branch = "manifest_branch" - build_target = "device_build_target-user" - gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - gsi_branch = "gsi_branch" - gsi_build_target = "gsi_build_target-user" - test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - test_branch = "gsi_branch" - test_build_target = "test_build_target-user" - - schedule = model.ScheduleModel() - schedule.schedule_type = "test" - schedule.test_name = "vts/vts" - schedule.manifest_branch = manifest_branch - schedule.build_target = build_target - schedule.device = ["test_lab1/product1"] - schedule.shards = 1 - schedule.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - schedule.gsi_storage_type = gsi_storage_type - schedule.gsi_branch = gsi_branch - schedule.gsi_build_target = gsi_build_target - schedule.test_storage_type = test_storage_type - schedule.test_branch = test_branch - schedule.test_build_target = test_build_target - schedule.require_signed_device_build = False - schedule.put() - - lab = model.LabModel() - lab.name = "test_lab1" - lab.hostname = "test_lab1_host1" - lab.owner = "test@google.com" + lab = self.GenerateLabModel() lab.put() - device = model.DeviceModel() - device.status = Status.DEVICE_STATUS_DICT["fastboot"] - device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT["free"] - device.product = "product1" - device.serial = "serial1" - device.hostname = "test_lab1_host1" + device = self.GenerateDeviceModel(hostname=lab.hostname) device.put() - # create a device build - build = model.BuildModel() - build.manifest_branch = manifest_branch - build.build_id = "1000000" - build.build_target = "device_build_target" - build.build_type = "user" - build.artifact_type = "device" - build.put() - - # create a gsi build - build = model.BuildModel() - build.manifest_branch = gsi_branch - build.build_id = "2000000" - build.build_target = "gsi_build_target" - build.build_type = "user" - build.artifact_type = "gsi" - build.put() - - # create a test build - build = model.BuildModel() - build.manifest_branch = gsi_branch - build.build_id = "3000000" - build.build_target = "test_build_target" - build.build_type = "user" - build.artifact_type = "test" - build.put() + schedule = self.GenerateScheduleModel( + device_model=device, lab_model=lab) + schedule.put() + + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() self.scheduler.post() self.assertEqual(1, len(model.JobModel.query().fetch())) print("A job is created successfully.") device_query = model.DeviceModel.query( - model.DeviceModel.serial == "serial1") + model.DeviceModel.serial == device.serial) device = device_query.fetch()[0] self.assertEqual(Status.DEVICE_SCHEDULING_STATUS_DICT["reserved"], device.scheduling_status) diff --git a/gae/webapp/src/tasks/indexing_test.py b/gae/webapp/src/tasks/indexing_test.py index 6930b3f..7ef8ea3 100644 --- a/gae/webapp/src/tasks/indexing_test.py +++ b/gae/webapp/src/tasks/indexing_test.py @@ -22,15 +22,12 @@ try: except ImportError: import mock -from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.tasks import indexing +from webapp.src.testing import unittest_base -from google.appengine.ext import ndb -from google.appengine.ext import testbed - -class IndexingHandlerTest(unittest.TestCase): +class IndexingHandlerTest(unittest_base.UnitTestBase): """Tests for IndexingHandler. Attributes: @@ -40,60 +37,16 @@ class IndexingHandlerTest(unittest.TestCase): def setUp(self): """Initializes test""" - # Create the Testbed class instance and initialize service stubs. - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - # Clear cache between tests. - ndb.get_context().clear_cache() + super(IndexingHandlerTest, self).setUp() # Mocking IndexingHandler. self.indexing_handler = indexing.IndexingHandler(mock.Mock()) self.indexing_handler.request = mock.Mock() - def tearDown(self): - self.testbed.deactivate() - def testSingleJobReindexing(self): """Asserts re-indexing links job and schedule successfully.""" - priority = "top" - test_name = "test/test" - period = 360 - build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - manifest_branch = "manifest_branch" - build_target = "device_build_target-user" - pab_account_id = "1234567890" - shards = 1 - retry_count = 2 - gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - gsi_branch = "gsi_branch" - gsi_build_target = "gsi_build_target-user" - gsi_pab_account_id = "1234567890" - test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - test_branch = "gsi_branch" - test_build_target = "gsi_build_target-user" - test_pab_account_id = "1234567890" - print("\n") - print("Creating a single schedule...") - schedule = model.ScheduleModel() - schedule.priority = priority - schedule.test_name = test_name - schedule.period = period - schedule.build_storage_type = build_storage_type - schedule.manifest_branch = manifest_branch - schedule.build_target = build_target - schedule.device_pab_account_id = pab_account_id - schedule.shards = shards - schedule.retry_count = retry_count - schedule.gsi_storage_type = gsi_storage_type - schedule.gsi_branch = gsi_branch - schedule.gsi_build_target = gsi_build_target - schedule.gsi_pab_account_id = gsi_pab_account_id - schedule.test_storage_type = test_storage_type - schedule.test_branch = test_branch - schedule.test_build_target = test_build_target - schedule.test_pab_account_id = test_pab_account_id + print("\nCreating a single schedule...") + schedule = self.GenerateScheduleModel() schedule.put() schedules = model.ScheduleModel.query().fetch() @@ -161,45 +114,10 @@ class IndexingHandlerTest(unittest.TestCase): def testMultiJobReindexing(self): """Asserts re-indexing links job and schedule successfully.""" - priority = "top" - test_name = "" - period = 360 - build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - manifest_branch = "manifest_branch" - build_target = "device_build_target-user" - pab_account_id = "1234567890" - shards = 1 - retry_count = 2 - gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - gsi_branch = "gsi_branch" - gsi_build_target = "gsi_build_target-user" - gsi_pab_account_id = "1234567890" - test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - test_branch = "gsi_branch" - test_build_target = "gsi_build_target-user" - test_pab_account_id = "1234567890" - print("\n") - - print("Creating four schedules...") + print("\nCreating four schedules...") for num in xrange(4): - schedule = model.ScheduleModel() - schedule.priority = priority - schedule.test_name = test_name + str(num + 1) - schedule.period = period - schedule.build_storage_type = build_storage_type - schedule.manifest_branch = manifest_branch - schedule.build_target = build_target - schedule.device_pab_account_id = pab_account_id - schedule.shards = shards - schedule.retry_count = retry_count - schedule.gsi_storage_type = gsi_storage_type - schedule.gsi_branch = gsi_branch - schedule.gsi_build_target = gsi_build_target - schedule.gsi_pab_account_id = gsi_pab_account_id - schedule.test_storage_type = test_storage_type - schedule.test_branch = test_branch - schedule.test_build_target = test_build_target - schedule.test_pab_account_id = test_pab_account_id + schedule = self.GenerateScheduleModel(test_name=str(num + 1)) + schedule.put() schedule.put() schedules = model.ScheduleModel.query().fetch() diff --git a/gae/webapp/src/testing/__init__.py b/gae/webapp/src/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gae/webapp/src/testing/unittest_base.py b/gae/webapp/src/testing/unittest_base.py new file mode 100644 index 0000000..c2a399b --- /dev/null +++ b/gae/webapp/src/testing/unittest_base.py @@ -0,0 +1,287 @@ +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import datetime +import random +import string +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from google.appengine.ext import ndb +from google.appengine.ext import testbed + +from webapp.src import vtslab_status as Status +from webapp.src.proto import model + + +class UnitTestBase(unittest.TestCase): + """Base class for unittest. + + Attributes: + testbed: A Testbed instance which provides local unit testing. + random_strs: a list of strings generated by GetRandomString() method + in order to avoid duplicates. + """ + random_strs = [] + + def setUp(self): + """Initializes unittest.""" + # Create the Testbed class instance and initialize service stubs. + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.init_datastore_v3_stub() + self.testbed.init_memcache_stub() + self.testbed.init_mail_stub() + self.testbed.setup_env(app_id="vtslab-schedule-unittest") + # Clear cache between tests. + ndb.get_context().clear_cache() + + def tearDown(self): + self.testbed.deactivate() + + def GetRandomString(self, length=7): + """Generates and returns a random string. + + Args: + length: an integer, string length. + + Returns: + a random string. + """ + new_str = "" + while new_str == "" or new_str in self.random_strs: + new_str = "".join( + random.choice(string.ascii_letters + string.digits) + for _ in range(length)) + return new_str + + def GenerateLabModel(self, lab_name=None, host_name=None): + """Builds model.LabModel with given information. + + Args: + lab_name: a string, lab name. + host_name: a string, host name. + + Returns: + model.LabModel instance. + """ + lab = model.LabModel() + lab.name = lab_name if lab_name else self.GetRandomString() + lab.hostname = host_name if host_name else self.GetRandomString() + lab.owner = "test@abc.com" + lab.ip = "100.100.100.100" + return lab + + def GenerateDeviceModel( + self, + hostname=None, + product=None, + serial=None, + status=Status.DEVICE_STATUS_DICT["fastboot"], + scheduling_status=Status.DEVICE_SCHEDULING_STATUS_DICT["free"]): + """Builds model.DeviceModel with given information. + + Args: + hostname: a string, host name. + product: a string, device product name. + serial: a string, device serial number. + status: an integer, device's initial status. + scheduling_status: an integer, device's initial scheduling status. + + Returns: + model.DeviceModel instance. + """ + device = model.DeviceModel() + device.hostname = hostname if hostname else self.GetRandomString() + device.product = product if product else self.GetRandomString() + device.serial = serial if serial else self.GetRandomString() + device.status = status + device.scheduling_status = scheduling_status + device.timestamp = datetime.datetime.now() + return device + + def GenerateScheduleModel( + self, + device_model=None, + lab_model=None, + priority="medium", + test_name=None, + period=360, + retry_count=1, + shards=1, + lab_name=None, + device_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + device_pab_account_id=None, + device_branch=None, + device_target=None, + gsi_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + gsi_pab_account_id=None, + gsi_branch=None, + gsi_build_target=None, + test_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + test_pab_account_id=None, + test_branch=None, + test_build_target=None, + required_signed_device_build=False): + """Builds model.ScheduleModel with given information. + + Args: + device_model: a model.DeviceModel instance to refer device product. + lab_model: a model.LabModel instance to refer host name. + priority: a string, scheduling priority + test_name: a string, schedule test name. + period: an integer, scheduling period. + retry_count: an integer, scheduling retry count. + shards: an integer, # ways of device shards. + lab_name: a string, target lab name. + device_storage_type: an integer, device storage type + device_pab_account_id: a string, device PAB account ID. + device_branch: a string, device build branch. + device_target: a string, device build target. + gsi_storage_type: an integer, GSI storage type + gsi_pab_account_id: a string, GSI PAB account ID. + gsi_branch: a string, GSI build branch. + gsi_build_target: a string, GSI build target. + test_storage_type: an integer, test storage type + test_pab_account_id: a string, test PAB account ID. + test_branch: a string, test build branch. + test_build_target: a string, test build target. + required_signed_device_build: a boolean, True to schedule for signed + device build, False if not. + + Returns: + model.ScheduleModel instance. + """ + + if device_model: + device_product = device_model.product + device_target = self.GetRandomString(4) + elif device_target: + device_product, device_target = device_target.split("-") + else: + device_product = self.GetRandomString(7) + device_target = self.GetRandomString(4) + + if lab_model: + lab = lab_model.name + elif lab_name: + lab = lab_name + else: + lab = self.GetRandomString() + + schedule = model.ScheduleModel() + schedule.priority = priority + schedule.test_name = test_name if test_name else self.GetRandomString() + schedule.period = period + schedule.shards = shards + schedule.retry_count = retry_count + schedule.build_storage_type = device_storage_type + schedule.required_signed_device_build = required_signed_device_build + schedule.manifest_branch = (device_branch if device_branch else + self.GetRandomString()) + schedule.build_target = "-".join([device_product, device_target]) + schedule.device_pab_account_id = (device_pab_account_id + if device_pab_account_id else + self.GetRandomString()) + schedule.gsi_storage_type = gsi_storage_type + schedule.gsi_branch = (gsi_branch + if gsi_branch else self.GetRandomString()) + schedule.gsi_build_target = (gsi_build_target + if gsi_build_target else "-".join([ + self.GetRandomString(), + self.GetRandomString(4) + ])) + schedule.gsi_pab_account_id = (gsi_pab_account_id if gsi_pab_account_id + else self.GetRandomString()) + schedule.test_storage_type = test_storage_type + schedule.test_branch = (test_branch + if test_branch else self.GetRandomString()) + schedule.test_build_target = (test_build_target + if test_build_target else "-".join([ + self.GetRandomString(), + self.GetRandomString(4) + ])) + schedule.test_pab_account_id = (test_pab_account_id + if test_pab_account_id else + self.GetRandomString()) + schedule.device = [] + schedule.device.append("/".join([lab, device_product])) + return schedule + + def GenerateBuildModel(self, schedule, targets=None): + """Builds model.BuildModel with given information. + + Args: + schedule: a model.ScheduleModel instance to look up build info. + targets: a list of strings which indicates artifact type. + + Returns: + model.BuildModel instance. + """ + build_dict = {} + if targets is None: + targets = ["device", "gsi", "test"] + for target in targets: + build = model.BuildModel() + build.artifact_type = target + build.timestamp = datetime.datetime.now() + if target == "device": + build.signed = schedule.required_signed_device_build + build.manifest_branch = schedule.manifest_branch + build.build_target, build.build_type = ( + schedule.build_target.split("-")) + elif target == "gsi": + build.manifest_branch = schedule.gsi_branch + build.build_target, build.build_type = ( + schedule.gsi_build_target.split("-")) + elif target == "test": + build.manifest_branch = schedule.test_branch + build.build_target, build.build_type = ( + schedule.test_build_target.split("-")) + build.build_id = self.GetNewBuildId(build) + build_dict[target] = build + return build_dict + + def GetNewBuildId(self, build): + """Generates build ID. + + This method always generates newest (higher number) build ID than other + builds stored in testbed datastore. + + Args: + build: a model.BuildModel instance to look up build information + from testbed datastore. + + Returns: + a string, build ID. + """ + format_string = "{0:07d}" + build_query = model.BuildModel.query( + model.BuildModel.artifact_type == build.artifact_type, + model.BuildModel.build_target == build.build_target, + model.BuildModel.signed == build.signed, + model.BuildModel.manifest_branch == build.manifest_branch) + exiting_builds = build_query.fetch() + if exiting_builds: + exiting_builds.sort(key=lambda x: x.build_id, reverse=True) + latest_build_id = int(exiting_builds[0].build_id) + return format_string.format(latest_build_id + 1) + else: + return format_string.format(1) diff --git a/gae/webapp/src/utils/model_util_test.py b/gae/webapp/src/utils/model_util_test.py index f86e2b8..c6180f9 100644 --- a/gae/webapp/src/utils/model_util_test.py +++ b/gae/webapp/src/utils/model_util_test.py @@ -26,34 +26,12 @@ except ImportError: from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.scheduler import schedule_worker +from webapp.src.testing import unittest_base from webapp.src.utils import model_util -from google.appengine.ext import ndb -from google.appengine.ext import testbed - -class ModelTest(unittest.TestCase): - """Tests for PeriodicJobHeartBeat cron class. - - Attributes: - testbed: A Testbed instance which provides local unit testing. - """ - - def setUp(self): - """Initializes test""" - # Create the Testbed class instance and initialize service stubs. - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.setup_env(app_id="vtslab-schedule-unittest") - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - self.testbed.init_mail_stub() - # Clear cache between tests. - ndb.get_context().clear_cache() - # import job_heartbeat after setting app_id. - - def tearDown(self): - self.testbed.deactivate() +class ModelTest(unittest_base.UnitTestBase): + """Tests for PeriodicJobHeartBeat cron class.""" def testJobAndScheduleModel(self): """Asserts JobModel and ScheduleModel. @@ -62,109 +40,27 @@ class ModelTest(unittest.TestCase): changed based on the status. This should not be applied before JobModel entity is updated to Datastore. """ - # schedule information - priority = "top" period = 360 - build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - manifest_branch = "manifest_branch" - build_target = "device_build_target-user" - pab_account_id = "1234567890" - shards = 1 - retry_count = 1 - gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - gsi_branch = "gsi_branch" - gsi_build_target = "gsi_build_target-user" - gsi_pab_account_id = "1234567890" - test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - test_branch = "test_branch" - test_build_target = "test_build_target-user" - test_pab_account_id = "1234567890" - - lab_name = "test_lab" - host_name = "test_host" - device_name = "device" - - # create a device build - build = model.BuildModel() - build.manifest_branch = manifest_branch - build.build_id = "1000000" - build.build_target = "device_build_target" - build.build_type = "user" - build.artifact_type = "device" - build.timestamp = datetime.datetime.now() - build.signed = False - build.put() - - # create a gsi build - build = model.BuildModel() - build.manifest_branch = gsi_branch - build.build_id = "2000000" - build.build_target = "gsi_build_target" - build.build_type = "user" - build.artifact_type = "gsi" - build.timestamp = datetime.datetime.now() - build.signed = False - build.put() - - # create a test build - build = model.BuildModel() - build.manifest_branch = test_branch - build.build_id = "3000000" - build.build_target = "test_build_target" - build.build_type = "user" - build.artifact_type = "test" - build.timestamp = datetime.datetime.now() - build.signed = False - build.put() - - # create a lab - lab = model.LabModel() - lab.name = lab_name - lab.hostname = host_name - lab.owner = "test@google.com" + + lab = self.GenerateLabModel() lab.put() - # create a device - device = model.DeviceModel() - device.hostname = host_name - device.product = device_name - device.serial = "serial" - device.status = Status.DEVICE_STATUS_DICT["fastboot"] - device.scheduling_status = ( - Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) - device.timestamp = datetime.datetime.now() + device = self.GenerateDeviceModel(hostname=lab.hostname) device.put() - # create a schedule - schedule = model.ScheduleModel() - schedule.priority = priority - schedule.test_name = "test/{}".format(device_name) - schedule.period = period - schedule.build_storage_type = build_storage_type - schedule.manifest_branch = manifest_branch - schedule.build_target = build_target - schedule.device_pab_account_id = pab_account_id - schedule.shards = shards - schedule.retry_count = retry_count - schedule.gsi_storage_type = gsi_storage_type - schedule.gsi_branch = gsi_branch - schedule.gsi_build_target = gsi_build_target - schedule.gsi_pab_account_id = gsi_pab_account_id - schedule.test_storage_type = test_storage_type - schedule.test_branch = test_branch - schedule.test_build_target = test_build_target - schedule.test_pab_account_id = test_pab_account_id - schedule.device = [] - schedule.device.append("{}/{}".format(lab_name, device_name)) + schedule = self.GenerateScheduleModel( + device_model=device, lab_model=lab, period=period) schedule.put() - schedule = model.ScheduleModel.query().fetch()[0] - schedule.put() + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() # Mocking ScheduleHandler and essential methods. scheduler = schedule_worker.ScheduleHandler(mock.Mock()) scheduler.response = mock.Mock() scheduler.response.write = mock.Mock() + scheduler.request.get = mock.MagicMock(return_value="") print("\nCreating a job...") scheduler.post() -- cgit v1.2.3 From 86da5b149465a600cb36b88c51afd0395a6e253f Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 4 Jun 2018 20:04:56 +0900 Subject: Apply new proto fields. Test: dev server Bug: 79905934, 109641545 Change-Id: I9bd11b49b78b7fdc11437a8b175bf3706a3a9991 --- gae/index.yaml | 13 +++++++++++++ gae/webapp/src/proto/model.py | 25 ++++++++++++++++++++++--- gae/webapp/src/scheduler/schedule_worker.py | 8 ++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index 61cf10b..e40428b 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -29,6 +29,8 @@ indexes: - name: build_target - name: test_name - name: require_signed_device_build + - name: has_bootloader_img + - name: has_radio_img - name: period - name: priority - name: device @@ -45,6 +47,11 @@ indexes: - name: children_jobs - name: suspended - name: error_count + - name: image_package_repo_base + - name: required_host_equipment + - name: required_device_equipment + - name: report_bucket + - name: report_spreadsheet_id - kind: LabModel ancestor: no @@ -85,3 +92,9 @@ indexes: - name: timestamp - name: parent_schedule - name: test_type + - name: require_signed_device_build + - name: has_bootloader_img + - name: has_radio_img + - name: image_package_repo_base + - name: report_bucket + - name: report_spreadsheet_id diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 3efaf0e..9a114dd 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -62,6 +62,8 @@ class ScheduleModel(ndb.Model): build_target = ndb.StringProperty() # type:name device_pab_account_id = ndb.StringProperty() require_signed_device_build = ndb.BooleanProperty() + has_bootloader_img = ndb.BooleanProperty() + has_radio_img = ndb.BooleanProperty() # GSI information gsi_storage_type = ndb.IntegerProperty() @@ -94,6 +96,9 @@ class ScheduleModel(ndb.Model): required_host_equipment = ndb.StringProperty(repeated=True) required_device_equipment = ndb.StringProperty(repeated=True) + report_bucket = ndb.StringProperty(repeated=True) + report_spreadsheet_id = ndb.StringProperty(repeated=True) + class ScheduleControlInfoMessage(messages.Message): """A message for representing a schedule control data entry.""" @@ -103,7 +108,7 @@ class ScheduleControlInfoMessage(messages.Message): class ScheduleInfoMessage(messages.Message): """A message for representing an individual schedule entry.""" - # Next ID = 27 + # Next ID = 31 # schedule name for green build schedule, optional. name = messages.StringField(16) schedule_type = messages.StringField(19) @@ -114,7 +119,8 @@ class ScheduleInfoMessage(messages.Message): build_target = messages.StringField(2) device_pab_account_id = messages.StringField(17) require_signed_device_build = messages.BooleanField(20) - + has_bootloader_img = messages.BooleanField(27) + has_radio_img = messages.BooleanField(28) # GSI information gsi_storage_type = messages.IntegerField(22) gsi_branch = messages.StringField(9) @@ -140,6 +146,9 @@ class ScheduleInfoMessage(messages.Message): required_host_equipment = messages.StringField(25, repeated=True) required_device_equipment = messages.StringField(26, repeated=True) + report_bucket = messages.StringField(29, repeated=True) + report_spreadsheet_id = messages.StringField(30, repeated=True) + class LabModel(ndb.Model): """A model for representing an individual lab entry.""" @@ -213,6 +222,8 @@ class JobModel(ndb.Model): priority = ndb.StringProperty() test_name = ndb.StringProperty() require_signed_device_build = ndb.BooleanProperty() + has_bootloader_img = ndb.BooleanProperty() + has_radio_img = ndb.BooleanProperty() device = ndb.StringProperty() serial = ndb.StringProperty(repeated=True) @@ -253,16 +264,21 @@ class JobModel(ndb.Model): image_package_repo_base = ndb.StringProperty() + report_bucket = ndb.StringProperty(repeated=True) + report_spreadsheet_id = ndb.StringProperty(repeated=True) + class JobMessage(messages.Message): """A message for representing an individual job entry.""" - # Next ID = 31 + # Next ID = 35 test_type = messages.IntegerField(29) hostname = messages.StringField(1) priority = messages.StringField(2) test_name = messages.StringField(3) require_signed_device_build = messages.BooleanField(23) + has_bootloader_img = messages.BooleanField(31) + has_radio_img = messages.BooleanField(32) device = messages.StringField(4) serial = messages.StringField(5, repeated=True) @@ -299,6 +315,9 @@ class JobMessage(messages.Message): image_package_repo_base = messages.StringField(30) + report_bucket = messages.StringField(33, repeated=True) + report_spreadsheet_id = messages.StringField(34, repeated=True) + class ReturnCodeMessage(messages.Enum): """Enum for default return code.""" diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 81e8026..7d49c60 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -236,6 +236,14 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.parent_schedule = schedule.key new_job.image_package_repo_base = ( schedule.image_package_repo_base) + new_job.required_host_equipment = ( + schedule.required_host_equipment) + new_job.required_device_equipment = ( + schedule.required_device_equipment) + new_job.has_bootloader_img = schedule.has_bootloader_img + new_job.has_radio_img = schedule.has_radio_img + new_job.report_bucket = schedule.report_bucket + new_job.report_spreadsheet_id = schedule.report_spreadsheet_id # uses bit 0-1 to indicate version. test_type = GetTestVersionType(schedule.manifest_branch, -- cgit v1.2.3 From 3c7350c3fe463a8ab63424e7eef1b2975ccbb705 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 5 Jun 2018 13:57:03 +0900 Subject: Set default values for newly added attributes. Test: dev server Bug: 79905934, 109641545 --- gae/webapp/src/proto/model.py | 4 ++-- gae/webapp/src/tasks/indexing.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 9a114dd..9775cfb 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -62,8 +62,8 @@ class ScheduleModel(ndb.Model): build_target = ndb.StringProperty() # type:name device_pab_account_id = ndb.StringProperty() require_signed_device_build = ndb.BooleanProperty() - has_bootloader_img = ndb.BooleanProperty() - has_radio_img = ndb.BooleanProperty() + has_bootloader_img = ndb.BooleanProperty(default=True) + has_radio_img = ndb.BooleanProperty(default=True) # GSI information gsi_storage_type = ndb.IntegerProperty() diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index 1a637c3..5ffbdb7 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -164,6 +164,9 @@ class IndexingHandler(webapp2.RequestHandler): x for x in entity.children_jobs if x] else: entity.children_jobs = [] + for attr in ["has_bootloader_img", "has_radio_img"]: + if getattr(entity, attr, None) is None: + setattr(entity, attr, True) else: pass to_put.append(entity) -- cgit v1.2.3 From 20b69706a177ad7ed4ac9c89e1a724be58899586 Mon Sep 17 00:00:00 2001 From: Hyunwoo Ko Date: Thu, 7 Jun 2018 13:53:49 +0900 Subject: Add has_bootloader/radio_img to the message when lease happens. Test: > device --lease=True, and then check the fetch commands. Bug: 109641545 --- gae/webapp/src/endpoint/job_queue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 285a593..26a33ab 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -103,6 +103,8 @@ class JobQueueApi(remote.Service): job_message.test_pab_account_id = job.test_pab_account_id job_message.test_type = job.test_type job_message.image_package_repo_base = job.image_package_repo_base + job_message.has_bootloader_img = job.has_bootloader_img + job_message.has_radio_img = job.has_radio_img device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) -- cgit v1.2.3 From a61a2b1780c356070e90450eab8c308367bf9642 Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 4 Jun 2018 20:07:41 +0900 Subject: Fix a bug when proto field is repeated but empty. Test: python testing/e2e_test.py Bug: 79717764 Change-Id: I7b2ded212a479d9e636b2616ea3644c1613966cc --- gae/webapp/src/endpoint/schedule_info.py | 37 +++++--- gae/webapp/src/endpoint/schedule_info_test.py | 124 ++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 gae/webapp/src/endpoint/schedule_info_test.py diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index 08f1b57..0a5861c 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Schedule Info APIs implemented using Google Cloud Endpoints.""" import datetime @@ -23,9 +22,7 @@ from google.appengine.ext import ndb from webapp.src.proto import model - -SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer( - model.ScheduleInfoMessage) +SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer(model.ScheduleInfoMessage) @endpoints.api(name="schedule_info", version="v1") @@ -41,12 +38,12 @@ class ScheduleInfoApi(remote.Service): def clear(self, request): """Clears test schedule info in DB.""" schedule_query = model.ScheduleModel.query( - model.ScheduleModel.schedule_type != "green" - ) + model.ScheduleModel.schedule_type != "green") existing_schedules = schedule_query.fetch(keys_only=True) if existing_schedules and len(existing_schedules) > 0: ndb.delete_multi(existing_schedules) - return model.DefaultResponse(return_code=model.ReturnCodeMessage.SUCCESS) + return model.DefaultResponse( + return_code=model.ReturnCodeMessage.SUCCESS) @endpoints.method( SCHEDULE_INFO_RESOURCE, @@ -64,7 +61,6 @@ class ScheduleInfoApi(remote.Service): exist_on_both = [ x for x in request_fields if x.name in model_attr_names ] - # check duplicates exclusions = [ "name", "schedule_type", "schedule", "param", "timestamp", @@ -74,18 +70,31 @@ class ScheduleInfoApi(remote.Service): duplicate_checklist = [ x for x in exist_on_both if x.name not in exclusions ] + empty_list_field = [] query = model.ScheduleModel.query() for field in duplicate_checklist: if field.repeated: - query = query.filter( - getattr(model.ScheduleModel, field.name).IN( - request.get_assigned_value(field.name))) + value = request.get_assigned_value(field.name) + if value: + query = query.filter( + getattr(model.ScheduleModel, field.name).IN( + request.get_assigned_value(field.name))) + else: + # empty list cannot be queried. + empty_list_field.append(field.name) else: query = query.filter( getattr(model.ScheduleModel, field.name) == request.get_assigned_value(field.name)) duplicated_schedules = query.fetch() + if empty_list_field: + duplicated_schedules = [ + schedule for schedule in duplicated_schedules + if all( + [not getattr(schedule, attr) for attr in empty_list_field]) + ] + if not duplicated_schedules: schedule = model.ScheduleModel() for field in exist_on_both: @@ -114,12 +123,12 @@ class GreenScheduleInfoApi(remote.Service): def clear(self, request): """Clears green build schedule info in DB.""" schedule_query = model.ScheduleModel.query( - model.ScheduleModel.schedule_type == "green" - ) + model.ScheduleModel.schedule_type == "green") existing_schedules = schedule_query.fetch(keys_only=True) if existing_schedules and len(existing_schedules) > 0: ndb.delete_multi(existing_schedules) - return model.DefaultResponse(return_code=model.ReturnCodeMessage.SUCCESS) + return model.DefaultResponse( + return_code=model.ReturnCodeMessage.SUCCESS) @endpoints.method( SCHEDULE_INFO_RESOURCE, diff --git a/gae/webapp/src/endpoint/schedule_info_test.py b/gae/webapp/src/endpoint/schedule_info_test.py new file mode 100644 index 0000000..d946733 --- /dev/null +++ b/gae/webapp/src/endpoint/schedule_info_test.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.endpoint import schedule_info +from webapp.src.proto import model +from webapp.src.testing import unittest_base + + +class ScheduleInfoTest(unittest_base.UnitTestBase): + """A class to test schedule_info endpoint API. + + Attributes: + scheduler: A mock schedule_worker.ScheduleHandler. + """ + + def setUp(self): + """Initializes test""" + super(ScheduleInfoTest, self).setUp() + + def testSetWithSimpleMessage(self): + """Asserts schedule_info/set API receives a simple message.""" + # As of June 8, 2018, these are uploaded from host controller. + message = model.ScheduleInfoMessage() + message.manifest_branch = self.GetRandomString() + message.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + message.build_target = self.GetRandomString() + message.require_signed_device_build = False + message.has_bootloader_img = True + message.has_radio_img = True + message.test_name = self.GetRandomString() + message.period = 360 + message.priority = "high" + message.device = [self.GetRandomString()] + message.required_host_equipment = [self.GetRandomString()] + message.required_device_equipment = [self.GetRandomString()] + message.device_pab_account_id = self.GetRandomString() + message.shards = 1 + message.param = [self.GetRandomString()] + message.retry_count = 1 + message.gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + message.gsi_branch = self.GetRandomString() + message.gsi_build_target = self.GetRandomString() + message.gsi_pab_account_id = self.GetRandomString() + message.gsi_vendor_version = self.GetRandomString() + message.test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + message.test_branch = self.GetRandomString() + message.test_build_target = self.GetRandomString() + message.test_pab_account_id = self.GetRandomString() + # message.image_package_repo_base = self.GetRandomString() + + container = ( + schedule_info.SCHEDULE_INFO_RESOURCE.combined_message_class()) + api = schedule_info.ScheduleInfoApi() + response = api.set(container) + + self.assertTrue(response.return_code, model.ReturnCodeMessage.SUCCESS) + + def testSetWithEmptyRepeatedField(self): + """Asserts schedule_info/set API receives a message. + + This test sets required_host_equipment to empty and sends to endpoint + method. + """ + # As of June 8, 2018, these are uploaded from host controller. + message = model.ScheduleInfoMessage() + message.manifest_branch = self.GetRandomString() + message.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + message.build_target = self.GetRandomString() + message.require_signed_device_build = False + message.has_bootloader_img = True + message.has_radio_img = True + message.test_name = self.GetRandomString() + message.period = 360 + message.priority = "high" + message.device = [self.GetRandomString()] + message.required_host_equipment = [] + message.required_device_equipment = [self.GetRandomString()] + message.device_pab_account_id = self.GetRandomString() + message.shards = 1 + message.param = [self.GetRandomString()] + message.retry_count = 1 + message.gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + message.gsi_branch = self.GetRandomString() + message.gsi_build_target = self.GetRandomString() + message.gsi_pab_account_id = self.GetRandomString() + message.gsi_vendor_version = self.GetRandomString() + message.test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] + message.test_branch = self.GetRandomString() + message.test_build_target = self.GetRandomString() + message.test_pab_account_id = self.GetRandomString() + # message.image_package_repo_base = self.GetRandomString() + + container = ( + schedule_info.SCHEDULE_INFO_RESOURCE.combined_message_class()) + api = schedule_info.ScheduleInfoApi() + response = api.set(container) + + self.assertTrue(response.return_code, model.ReturnCodeMessage.SUCCESS) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From b048106f4541206fdf1ce68a412b8a037c748e2b Mon Sep 17 00:00:00 2001 From: Jongmok Date: Tue, 5 Jun 2018 10:57:28 +0900 Subject: Apply priority scheduling with aging. Test: unittest Bug: 109709787 Change-Id: I63f0717c0bfc1dd2e0c3bfaa509d83ad83fc7d44 --- gae/index.yaml | 1 + gae/webapp/src/dashboard/job_list.py | 2 +- gae/webapp/src/endpoint/job_queue.py | 2 +- gae/webapp/src/endpoint/schedule_info.py | 2 + gae/webapp/src/proto/model.py | 1 + gae/webapp/src/scheduler/schedule_worker.py | 402 ++++++++++++----------- gae/webapp/src/scheduler/schedule_worker_test.py | 219 ++++++++++++ gae/webapp/src/tasks/indexing.py | 6 + gae/webapp/src/testing/unittest_base.py | 40 +++ gae/webapp/src/vtslab_status.py | 21 +- 10 files changed, 500 insertions(+), 196 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index e40428b..d66ceb0 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -33,6 +33,7 @@ indexes: - name: has_radio_img - name: period - name: priority + - name: priority_value - name: device - name: shards - name: param diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index 16d1c46..5488c64 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -180,7 +180,7 @@ class CreateJobPage(JobBase): new_job = model.JobModel() new_job.hostname = self.request.get("hostname", default_value="") - new_job.priority = str(vtslab_status.PrioritySortHelper( + new_job.priority = str(vtslab_status.GetPriorityValue( self.request.get("priority", default_value="high"))) new_job.test_name = self.request.get("test_name", default_value="") new_job.device = self.request.get("device", default_value="") diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 26a33ab..652d98e 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -50,7 +50,7 @@ class JobQueueApi(remote.Service): priority_sorted_jobs = sorted( existing_jobs, - key=lambda x: (Status.PrioritySortHelper(x.priority), x.timestamp)) + key=lambda x: (Status.GetPriorityValue(x.priority), x.timestamp)) job_message = model.JobMessage() job_message.hostname = "" diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index 08f1b57..5be4724 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -21,6 +21,7 @@ from protorpc import remote from google.appengine.ext import ndb +from webapp.src import vtslab_status as Status from webapp.src.proto import model @@ -95,6 +96,7 @@ class ScheduleInfoApi(remote.Service): schedule.schedule_type = "test" schedule.error_count = 0 schedule.suspended = False + schedule.priority_value = Status.GetPriorityValue(schedule.priority) schedule.put() return model.DefaultResponse( diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 9775cfb..8912f7b 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -82,6 +82,7 @@ class ScheduleModel(ndb.Model): period = ndb.IntegerProperty() schedule = ndb.StringProperty() priority = ndb.StringProperty() + priority_value = ndb.IntegerProperty() device = ndb.StringProperty(repeated=True) shards = ndb.IntegerProperty() param = ndb.StringProperty(repeated=True) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 7d49c60..c09f2e2 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -16,6 +16,7 @@ # import datetime +import itertools import logging import re @@ -24,11 +25,14 @@ from google.appengine.ext import ndb from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.utils import logger -from webapp.src.utils import model_util import webapp2 MAX_LOG_CHARACTERS = 10000 # maximum number of characters per each log +CREATE_JOB_SUCCESS = "success" +CREATE_JOB_FAILED_NO_BUILD = "no_build" +CREATE_JOB_FAILED_NO_DEVICE = "no_device" + def GetTestVersionType(manifest_branch, gsi_branch, test_type=0): """Compares manifest branch and gsi branch to get test type. @@ -183,132 +187,53 @@ class ScheduleHandler(webapp2.RequestHandler): schedules = schedule_query.fetch() if schedules: - for schedule in schedules: - self.logger.Println("") - self.logger.Println("Schedule: %s (branch: %s)" % - (schedule.test_name, - schedule.manifest_branch)) - self.logger.Println("Build Target: %s" % schedule.build_target) - self.logger.Println("Device: %s" % schedule.device) - self.logger.Indent() - if not manual_job and not self.NewPeriod(schedule): - self.logger.Println("- Skipped") - self.logger.Unindent() - continue - - target_host, target_device, target_device_serials = ( - self.SelectTargetLab(schedule)) - if not target_host: - self.logger.Unindent() - continue + schedules = self.FilterWithPeriod(schedules) - self.logger.Println("- Target host: %s" % target_host) - self.logger.Println("- Target device: %s" % target_device) - self.logger.Println( - "- Target serials: %s" % target_device_serials) - - # create job and add. - new_job = model.JobModel() - new_job.hostname = target_host - new_job.priority = schedule.priority - new_job.test_name = schedule.test_name - new_job.require_signed_device_build = ( - schedule.require_signed_device_build) - new_job.device = target_device - new_job.period = schedule.period - new_job.serial.extend(target_device_serials) - new_job.build_storage_type = schedule.build_storage_type - new_job.manifest_branch = schedule.manifest_branch - new_job.build_target = schedule.build_target - new_job.pab_account_id = schedule.device_pab_account_id - new_job.shards = schedule.shards - new_job.param = schedule.param - new_job.retry_count = schedule.retry_count - new_job.gsi_storage_type = schedule.gsi_storage_type - new_job.gsi_branch = schedule.gsi_branch - new_job.gsi_build_target = schedule.gsi_build_target - new_job.gsi_pab_account_id = schedule.gsi_pab_account_id - new_job.gsi_vendor_version = schedule.gsi_vendor_version - new_job.test_storage_type = schedule.test_storage_type - new_job.test_branch = schedule.test_branch - new_job.test_build_target = schedule.test_build_target - new_job.test_pab_account_id = schedule.test_pab_account_id - new_job.parent_schedule = schedule.key - new_job.image_package_repo_base = ( - schedule.image_package_repo_base) - new_job.required_host_equipment = ( - schedule.required_host_equipment) - new_job.required_device_equipment = ( - schedule.required_device_equipment) - new_job.has_bootloader_img = schedule.has_bootloader_img - new_job.has_radio_img = schedule.has_radio_img - new_job.report_bucket = schedule.report_bucket - new_job.report_spreadsheet_id = schedule.report_spreadsheet_id - - # uses bit 0-1 to indicate version. - test_type = GetTestVersionType(schedule.manifest_branch, - schedule.gsi_branch) - # uses bit 2 - if schedule.require_signed_device_build: - test_type |= Status.TEST_TYPE_DICT[Status.TEST_TYPE_SIGNED] - new_job.test_type = test_type - - if manual_job: - test_type |= Status.TEST_TYPE_DICT[Status.TEST_TYPE_MANUAL] - - new_job.build_id = "" - new_job.gsi_build_id = "" - new_job.test_build_id = "" - for artifact_type in ["device", "gsi", "test"]: - if artifact_type == "device": - storage_type_text = "build_storage_type" - manifest_branch_text = "manifest_branch" - build_target_text = "build_target" - build_id_text = "build_id" - signed = new_job.require_signed_device_build - else: - storage_type_text = artifact_type + "_storage_type" - manifest_branch_text = artifact_type + "_branch" - build_target_text = artifact_type + "_build_target" - build_id_text = artifact_type + "_build_id" - signed = False - - manifest_branch = getattr(new_job, manifest_branch_text) - build_target = getattr(new_job, build_target_text) - storage_type = getattr(new_job, storage_type_text) - if storage_type == Status.STORAGE_TYPE_DICT["PAB"]: - build_id = self.FindBuildId( - artifact_type=artifact_type, - manifest_branch=manifest_branch, - target=build_target, - signed=signed) - elif storage_type == Status.STORAGE_TYPE_DICT["GCS"]: - # temp value to distinguish from empty values. - build_id = "gcs" + if schedules: + schedules.sort(key=lambda x: self.GetProductName(x)) + group_by_product = [ + list(g) + for _, g in itertools.groupby(schedules, + lambda x: self.GetProductName(x)) + ] + for group in group_by_product: + group.sort(key=lambda x: x.priority_value if ( + x.priority_value) else Status.GetPriorityValue(x.priority)) + create_result = { + CREATE_JOB_SUCCESS: [], + CREATE_JOB_FAILED_NO_BUILD: [], + CREATE_JOB_FAILED_NO_DEVICE: [] + } + for schedule in group: + self.logger.Println("") + self.logger.Println("Schedule: %s (branch: %s)" % + (schedule.test_name, + schedule.manifest_branch)) + self.logger.Println( + "Build Target: %s" % schedule.build_target) + self.logger.Println("Device: %s" % schedule.device) + self.logger.Indent() + result, lab = self.CreateJob(schedule, manual_job) + if result == CREATE_JOB_SUCCESS: + create_result[result].append(lab) else: - build_id = "" - self.logger.Println( - "Unexpected storage type (%s)." % storage_type) - setattr(new_job, build_id_text, build_id) - - if ((not new_job.manifest_branch or new_job.build_id) - and (not new_job.gsi_branch or new_job.gsi_build_id) - and - (not new_job.test_branch or new_job.test_build_id)): - new_job.build_id = new_job.build_id.replace("gcs", "") - new_job.gsi_build_id = (new_job.gsi_build_id.replace( - "gcs", "")) - new_job.test_build_id = (new_job.test_build_id.replace( - "gcs", "")) - self.ReserveDevices(target_device_serials) - new_job.status = Status.JOB_STATUS_DICT["ready"] - new_job.timestamp = datetime.datetime.now() - new_job_key = new_job.put() - schedule.children_jobs.append(new_job_key) - schedule.put() - self.logger.Println("A new job has been created.") - - self.logger.Unindent() + create_result[result].append(schedule) + self.logger.Unindent() + # if any schedule in group created a job, increase priority of + # the schedules which couldn't create due to out of devices. + schedules_to_put = [] + for lab in create_result[CREATE_JOB_SUCCESS]: + for schedule in create_result[CREATE_JOB_FAILED_NO_DEVICE]: + if any([lab in target for target in schedule.device + ]) and schedule not in schedules_to_put: + if schedule.priority_value is None: + schedule.priority_value = ( + Status.GetPriorityValue(schedule.priority)) + if schedule.priority_value > 0: + schedule.priority_value -= 1 + schedules_to_put.append(schedule) + if schedules_to_put: + ndb.put_multi(schedules_to_put) self.logger.Println("Scheduling completed.") @@ -325,71 +250,163 @@ class ScheduleHandler(webapp2.RequestHandler): outputs.append(line) logging.info("\n".join(outputs)) - def NewPeriod(self, schedule): - """Checks whether a new job creation is needed. + def CreateJob(self, schedule, manual_job=False): + """Creates a job for given schedule. Args: - schedule: a proto containing schedule information. + schedule: model.ScheduleModel instance. + manual_job: True if a job is created by a user, False otherwise. Returns: - True if new job is required, False otherwise. + a string of job creation result message. + a string of lab name if job is created, otherwise empty string. """ - job_query = model.JobModel.query( - model.JobModel.manifest_branch == schedule.manifest_branch, - model.JobModel.build_target == schedule.build_target, - model.JobModel.test_name == schedule.test_name, - model.JobModel.period == schedule.period, - model.JobModel.shards == schedule.shards, - model.JobModel.retry_count == schedule.retry_count, - model.JobModel.gsi_branch == schedule.gsi_branch, - model.JobModel.test_branch == schedule.test_branch) - same_jobs = job_query.fetch() - same_jobs = [ - x for x in same_jobs - if (set(x.param) == set(schedule.param) - and x.device in schedule.device) - ] - if not same_jobs: - return True - - outdated_jobs = [ - x for x in same_jobs - if (datetime.datetime.now() - x.timestamp > datetime.timedelta( - minutes=x.period)) - ] - outdated_ready_jobs = [ - x for x in outdated_jobs - if x.status == Status.JOB_STATUS_DICT["expired"] - ] - - if outdated_ready_jobs: - self.logger.Println( - ("Job key[{}] is(are) outdated. " - "They became infra-err status.").format( - ", ".join( - [str(x.key.id()) for x in outdated_ready_jobs]))) - for job in outdated_ready_jobs: - job.status = Status.JOB_STATUS_DICT["infra-err"] - job.put() - model_util.UpdateParentSchedule(job, job.status) - - outdated_leased_jobs = [ - x for x in outdated_jobs - if x.status == Status.JOB_STATUS_DICT["leased"] - ] - if outdated_leased_jobs: - self.logger.Println( - ("Job key[{}] is(are) expected to be completed " - "however still in leased status.").format( - ", ".join( - [str(x.key.id()) for x in outdated_leased_jobs]))) - - recent_jobs = [x for x in same_jobs if x not in outdated_jobs] - - if recent_jobs or outdated_leased_jobs: - return False + target_host, target_device, target_device_serials = ( + self.SelectTargetLab(schedule)) + if not target_host: + return CREATE_JOB_FAILED_NO_DEVICE, "" + + self.logger.Println("- Target host: %s" % target_host) + self.logger.Println("- Target device: %s" % target_device) + self.logger.Println( + "- Target serials: %s" % target_device_serials) + + # create job and add. + new_job = model.JobModel() + new_job.hostname = target_host + new_job.priority = schedule.priority + new_job.test_name = schedule.test_name + new_job.require_signed_device_build = ( + schedule.require_signed_device_build) + new_job.device = target_device + new_job.period = schedule.period + new_job.serial.extend(target_device_serials) + new_job.build_storage_type = schedule.build_storage_type + new_job.manifest_branch = schedule.manifest_branch + new_job.build_target = schedule.build_target + new_job.pab_account_id = schedule.device_pab_account_id + new_job.shards = schedule.shards + new_job.param = schedule.param + new_job.retry_count = schedule.retry_count + new_job.gsi_storage_type = schedule.gsi_storage_type + new_job.gsi_branch = schedule.gsi_branch + new_job.gsi_build_target = schedule.gsi_build_target + new_job.gsi_pab_account_id = schedule.gsi_pab_account_id + new_job.gsi_vendor_version = schedule.gsi_vendor_version + new_job.test_storage_type = schedule.test_storage_type + new_job.test_branch = schedule.test_branch + new_job.test_build_target = schedule.test_build_target + new_job.test_pab_account_id = schedule.test_pab_account_id + new_job.parent_schedule = schedule.key + new_job.image_package_repo_base = schedule.image_package_repo_base + new_job.required_host_equipment = schedule.required_host_equipment + new_job.required_device_equipment = schedule.required_device_equipment + new_job.has_bootloader_img = schedule.has_bootloader_img + new_job.has_radio_img = schedule.has_radio_img + new_job.report_bucket = schedule.report_bucket + new_job.report_spreadsheet_id = schedule.report_spreadsheet_id + + # uses bit 0-1 to indicate version. + test_type = GetTestVersionType(schedule.manifest_branch, + schedule.gsi_branch) + # uses bit 2 + if schedule.require_signed_device_build: + test_type |= Status.TEST_TYPE_DICT[Status.TEST_TYPE_SIGNED] + + if manual_job: + test_type |= Status.TEST_TYPE_DICT[Status.TEST_TYPE_MANUAL] + + new_job.test_type = test_type + + new_job.build_id = "" + new_job.gsi_build_id = "" + new_job.test_build_id = "" + for artifact_type in ["device", "gsi", "test"]: + if artifact_type == "device": + storage_type_text = "build_storage_type" + manifest_branch_text = "manifest_branch" + build_target_text = "build_target" + build_id_text = "build_id" + signed = new_job.require_signed_device_build + else: + storage_type_text = artifact_type + "_storage_type" + manifest_branch_text = artifact_type + "_branch" + build_target_text = artifact_type + "_build_target" + build_id_text = artifact_type + "_build_id" + signed = False + + manifest_branch = getattr(new_job, manifest_branch_text) + build_target = getattr(new_job, build_target_text) + storage_type = getattr(new_job, storage_type_text) + if storage_type == Status.STORAGE_TYPE_DICT["PAB"]: + build_id = self.FindBuildId( + artifact_type=artifact_type, + manifest_branch=manifest_branch, + target=build_target, + signed=signed) + elif storage_type == Status.STORAGE_TYPE_DICT["GCS"]: + # temp value to distinguish from empty values. + build_id = "gcs" + else: + build_id = "" + self.logger.Println( + "Unexpected storage type (%s)." % storage_type) + setattr(new_job, build_id_text, build_id) + + if ((not new_job.manifest_branch or new_job.build_id) + and (not new_job.gsi_branch or new_job.gsi_build_id) + and (not new_job.test_branch or new_job.test_build_id)): + new_job.build_id = new_job.build_id.replace("gcs", "") + new_job.gsi_build_id = (new_job.gsi_build_id.replace("gcs", "")) + new_job.test_build_id = (new_job.test_build_id.replace("gcs", "")) + self.ReserveDevices(target_device_serials) + new_job.status = Status.JOB_STATUS_DICT["ready"] + new_job.timestamp = datetime.datetime.now() + new_job_key = new_job.put() + schedule.children_jobs.append(new_job_key) + schedule.priority_value = Status.GetPriorityValue( + schedule.priority) + schedule.put() + self.logger.Println("A new job has been created.") + labs = model.LabModel.query( + model.LabModel.hostname == target_host).fetch() + return CREATE_JOB_SUCCESS, labs[0].name else: - return True + return CREATE_JOB_FAILED_NO_BUILD, "" + + def FilterWithPeriod(self, schedules): + """Filters schedules with period. + + This method filters schedules if any children jobs are created within + period time. + + Args: + schedules: a list of model.ScheduleModel instances. + + Returns: + a list of model.ScheduleModel instances which need to create a new + job. + """ + ret_list = [] + if not schedules: + return ret_list + + if type(schedules) is not list: + schedules = [schedules] + + for schedule in schedules: + if not schedule.children_jobs: + ret_list.append(schedule) + continue + + latest_job_key = schedule.children_jobs[-1] + latest_job = latest_job_key.get() + + if datetime.datetime.now() - latest_job.timestamp > ( + datetime.timedelta(minutes=schedule.period)): + ret_list.append(schedule) + + return ret_list def SelectTargetLab(self, schedule): """Find target host and devices to schedule a new job. @@ -456,3 +473,20 @@ class ScheduleHandler(webapp2.RequestHandler): (schedule.shards, available_devices)) self.logger.Unindent() return None, None, [] + + def GetProductName(self, schedule): + """Gets a product name from schedule instance. + + Args: + schedule: a schedule instance. + + Returns: + a string, product name in lowercase. + """ + if not schedule or not schedule.device: + return "" + + if "/" not in schedule.device[0]: + return "" + + return schedule.device[0].split("/")[1].lower() diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index 1e07417..c639fa6 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -75,6 +75,225 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): device.scheduling_status) print("A device is reserved successfully.") + def testPriorityScheduling(self): + """Asserts job creation with priority scheduling.""" + product = "product" + high_priority_schedule_test_name = "high_test" + medium_priority_schedule_test_name = "medium_test" + + lab = self.GenerateLabModel() + lab.put() + + device = self.GenerateDeviceModel( + hostname=lab.hostname, product=product) + device.put() + + schedule_high = self.GenerateScheduleModel( + device_model=device, + lab_model=lab, + priority="high", + test_name=high_priority_schedule_test_name) + schedule_high.put() + + schedule_medium = self.GenerateScheduleModel( + device_model=device, + lab_model=lab, + priority="medium", + test_name=medium_priority_schedule_test_name) + schedule_medium.put() + + build_dict = self.GenerateBuildModel(schedule_high) + for key in build_dict: + build_dict[key].put() + + self.scheduler.post() + schedules = model.ScheduleModel.query().fetch() + self.assertEqual(schedules[0].test_name, + high_priority_schedule_test_name) + + def testPrioritySchedulingWithAging(self): + """Asserts job creation with priority scheduling with aging.""" + product = "product" + high_priority_schedule_test_name = "high_test" + medium_priority_schedule_test_name = "medium_test" + schedule_period_minute = 100 + + lab = self.GenerateLabModel() + lab.put() + + device = self.GenerateDeviceModel( + hostname=lab.hostname, product=product) + device.put() + + schedules = [] + schedule_high = self.GenerateScheduleModel( + device_model=device, + lab_model=lab, + test_name=high_priority_schedule_test_name, + period=schedule_period_minute, + priority="high") + schedule_high.put() + schedules.append(schedule_high) + + schedule_medium = self.GenerateScheduleModel( + device_model=device, + lab_model=lab, + test_name=medium_priority_schedule_test_name, + period=schedule_period_minute, + priority="medium") + schedule_medium.put() + schedules.append(schedule_medium) + + for schedule in schedules: + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() + + high_original_priority_value = schedule_high.priority_value + medium_original_priority_value = schedule_medium.priority_value + + # On first attempt, "high" priority will create a job. + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(jobs[0].test_name, high_priority_schedule_test_name) + + # medium priority schedule's priority value will be decreased. + self.assertEqual(medium_original_priority_value - 1, + schedule_medium.priority_value) + + self.PassTime(minutes=schedule_period_minute + 1) + self.ResetDevices() + + # On second attempt, "high" priority will create a job. + self.scheduler.post() + jobs = model.JobModel.query().fetch() + jobs.sort(key=lambda x: x.timestamp, reverse=True) # latest first + self.assertEqual(jobs[0].test_name, high_priority_schedule_test_name) + + # medium priority schedule's priority value will be decreased again. + self.assertEqual(medium_original_priority_value - 2, + schedule_medium.priority_value) + + while schedule_medium.priority_value >= high_original_priority_value: + self.PassTime(minutes=schedule_period_minute + 1) + self.ResetDevices() + self.scheduler.post() + + # at last, medium priority schedule should be able to create a job. + self.PassTime(minutes=schedule_period_minute + 1) + self.ResetDevices() + self.scheduler.post() + + jobs = model.JobModel.query().fetch() + jobs.sort(key=lambda x: x.timestamp, reverse=True) # latest first + self.assertEqual(jobs[0].test_name, medium_priority_schedule_test_name) + + # after a job is created, its priority value should be restored. + self.assertEqual(schedule_medium.priority_value, + medium_original_priority_value) + + def testPrioritySchedulingWithAgingForMultiDevices(self): + """Asserts job creation with priority scheduling for multi devices.""" + product1 = "product1" + product2 = "product2" + schedule_period_minute = 360 + + lab = self.GenerateLabModel() + lab.put() + + device1 = self.GenerateDeviceModel( + hostname=lab.hostname, product=product1) + device1.put() + + device2 = self.GenerateDeviceModel( + hostname=lab.hostname, product=product2) + device2.put() + + schedule1_l = self.GenerateScheduleModel( + device_model=device1, + lab_model=lab, + priority="low", + period=schedule_period_minute) + schedule1_l.put() + + schedule1_h = self.GenerateScheduleModel( + device_model=device1, + lab_model=lab, + priority="high", + period=schedule_period_minute) + schedule1_h.put() + + schedule2_m = self.GenerateScheduleModel( + device_model=device2, + lab_model=lab, + priority="medium", + period=schedule_period_minute) + schedule2_m.put() + + schedule2_h = self.GenerateScheduleModel( + device_model=device2, + lab_model=lab, + priority="high", + period=schedule_period_minute) + schedule2_h.put() + + schedule1_l_original_priority_value = schedule1_l.priority_value + schedule2_m_original_priority_value = schedule2_m.priority_value + + for schedule in [schedule2_m, schedule2_h]: + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() + + # create jobs + self.scheduler.post() + + # schedule2_m will not get a change to create a job. + jobs = model.JobModel.query().fetch() + self.assertTrue( + any([job.test_name == schedule2_h.test_name for job in jobs])) + self.assertFalse( + any([job.test_name == schedule2_m.test_name for job in jobs])) + + # schedule2_m's priority value should be decreased. + self.assertTrue(schedule2_m_original_priority_value - 1, + schedule2_m.priority_value) + + # schedule1_l's priority value should not be changed because all other + # schedules for device1 were also failed to created a job. + self.assertTrue(schedule1_l_original_priority_value, + schedule1_l.priority_value) + + for num in range(3): + self.assertTrue(schedule2_m_original_priority_value - 1 - num, + schedule2_m.priority_value) + self.PassTime(minutes=schedule_period_minute + 1) + self.ResetDevices() + self.scheduler.post() + self.assertFalse( + any([job.test_name == schedule2_m.test_name for job in jobs])) + self.assertTrue(schedule1_l_original_priority_value, + schedule1_l.priority_value) + + # device1 is ready for scheduling. + for schedule in [schedule1_l, schedule1_h]: + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() + + # after 4 times of failure, now schedule2_m can create a job. + self.PassTime(minutes=schedule_period_minute + 1) + self.ResetDevices() + self.scheduler.post() + + jobs = model.JobModel.query().fetch() + self.assertTrue( + any([job.test_name == schedule2_m.test_name for job in jobs])) + + # now schedule_1's priority value should be changed. + self.assertEquals(schedule1_l_original_priority_value - 1, + schedule1_l.priority_value) + if __name__ == "__main__": unittest.main() diff --git a/gae/webapp/src/tasks/indexing.py b/gae/webapp/src/tasks/indexing.py index 5ffbdb7..2ebe687 100644 --- a/gae/webapp/src/tasks/indexing.py +++ b/gae/webapp/src/tasks/indexing.py @@ -164,9 +164,15 @@ class IndexingHandler(webapp2.RequestHandler): x for x in entity.children_jobs if x] else: entity.children_jobs = [] + for attr in ["has_bootloader_img", "has_radio_img"]: if getattr(entity, attr, None) is None: setattr(entity, attr, True) + + # set priority_value for old schedules. + if entity.priority_value is None: + entity.priority_value = Status.GetPriorityValue( + entity.priority) else: pass to_put.append(entity) diff --git a/gae/webapp/src/testing/unittest_base.py b/gae/webapp/src/testing/unittest_base.py index c2a399b..684f692 100644 --- a/gae/webapp/src/testing/unittest_base.py +++ b/gae/webapp/src/testing/unittest_base.py @@ -223,6 +223,7 @@ class UnitTestBase(unittest.TestCase): self.GetRandomString()) schedule.device = [] schedule.device.append("/".join([lab, device_product])) + schedule.priority_value = Status.GetPriorityValue(schedule.priority) return schedule def GenerateBuildModel(self, schedule, targets=None): @@ -285,3 +286,42 @@ class UnitTestBase(unittest.TestCase): return format_string.format(latest_build_id + 1) else: return format_string.format(1) + + def PassTime(self, hours=0, minutes=0, seconds=0): + """Assumes that a certain amount of time has passed. + + This method changes does not change actual system time but changes all + jobs timestamp to assume time has passed. + + Args: + hours: an integer, number of hours to pass time. + minutes: an integer, number of minutes to pass time. + seconds: an integer, number of seconds to pass time. + """ + if not hours and not minutes and not seconds: + return + + jobs = model.JobModel.query().fetch() + to_put = [] + for job in jobs: + if job.timestamp: + job.timestamp -= datetime.timedelta( + hours=hours, minutes=minutes, seconds=seconds) + if job.heartbeat_stamp: + job.heartbeat_stamp -= datetime.timedelta( + hours=hours, minutes=minutes, seconds=seconds) + to_put.append(job) + if to_put: + ndb.put_multi(to_put) + + def ResetDevices(self): + """Resets all devices to ready status.""" + devices = model.DeviceModel.query().fetch() + to_put = [] + for device in devices: + device.status = Status.DEVICE_STATUS_DICT["fastboot"] + device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ + "free"] + to_put.append(device) + if to_put: + ndb.put_multi(to_put) diff --git a/gae/webapp/src/vtslab_status.py b/gae/webapp/src/vtslab_status.py index 7ed70ad..967bbf0 100644 --- a/gae/webapp/src/vtslab_status.py +++ b/gae/webapp/src/vtslab_status.py @@ -58,11 +58,11 @@ JOB_STATUS_DICT = { } JOB_PRIORITY_DICT = { - "top": 0, - "high": 1, - "medium": 2, - "low": 3, - "other": 4 + "top": 3, + "high": 6, + "medium": 9, + "low": 12, + "other": 15 } @@ -99,7 +99,7 @@ TEST_TYPE_DICT = { } -def PrioritySortHelper(priority): +def GetPriorityValue(priority): """Helper function to sort jobs based on priority. Args: @@ -108,7 +108,8 @@ def PrioritySortHelper(priority): Returns: int, priority order (the lower, the higher) """ - priority = priority.lower() - if priority in JOB_PRIORITY_DICT: - return JOB_PRIORITY_DICT[priority] - return 4 + if priority: + priority = priority.lower() + if priority in JOB_PRIORITY_DICT: + return JOB_PRIORITY_DICT[priority] + return JOB_PRIORITY_DICT["other"] -- cgit v1.2.3 From e69279b0eaa0c62e65588c3f2abae4a3ea0e520f Mon Sep 17 00:00:00 2001 From: Jongmok Date: Fri, 8 Jun 2018 13:49:13 +0900 Subject: Display equipment value on schedule webpage. Test: dev server Bug: 79541234 --- gae/webapp/static/device.html | 3 +++ gae/webapp/static/schedule.html | 3 +++ 2 files changed, 6 insertions(+) diff --git a/gae/webapp/static/device.html b/gae/webapp/static/device.html index 1d2f676..b674355 100644 --- a/gae/webapp/static/device.html +++ b/gae/webapp/static/device.html @@ -94,6 +94,7 @@
Serial Status Scheduling Status + equipment Timestamp
+ {{ device.device_equipment }} {% if device.timestamp %} {{ convert_time(device.timestamp).strftime("%Y-%m-%d %H:%M:%S") }} diff --git a/gae/webapp/static/schedule.html b/gae/webapp/static/schedule.html index 791c5c1..9482e5c 100644 --- a/gae/webapp/static/schedule.html +++ b/gae/webapp/static/schedule.html @@ -71,6 +71,7 @@ retry_count param priority + required host/device equipment timestamp resume/suspend create a job @@ -99,6 +100,8 @@ {{ schedule.param }} {{ schedule.priority }} + + {{ schedule.required_host_equipment }} / {{ schedule.required_device_equipment }} {% if schedule.timestamp %} {{ convert_time(schedule.timestamp).strftime("%Y-%m-%d %H:%M:%S") }} -- cgit v1.2.3 From e24a8c54b328b544774b9f9a5475166a69f9907e Mon Sep 17 00:00:00 2001 From: Jongmok Date: Mon, 4 Jun 2018 20:33:34 +0900 Subject: Retry with shorten period after boot-up error. Test: python testing/e2e_test.py Bug: 79917402 Change-Id: I9a1d67408d2ac2e8b72a932387c25db07bdb9ec5 --- gae/webapp/src/scheduler/schedule_worker.py | 28 ++++++++- gae/webapp/src/scheduler/schedule_worker_test.py | 76 ++++++++++++++++++++++++ gae/webapp/src/utils/model_util.py | 2 +- gae/webapp/src/vtslab_status.py | 3 + 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index c09f2e2..fcb0f13 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -28,6 +28,7 @@ from webapp.src.utils import logger import webapp2 MAX_LOG_CHARACTERS = 10000 # maximum number of characters per each log +BOOTUP_ERROR_RETRY_INTERVAL_IN_MINS = 60 # retry minutes when boot-up error is occurred CREATE_JOB_SUCCESS = "success" CREATE_JOB_FAILED_NO_BUILD = "no_build" @@ -268,8 +269,7 @@ class ScheduleHandler(webapp2.RequestHandler): self.logger.Println("- Target host: %s" % target_host) self.logger.Println("- Target device: %s" % target_device) - self.logger.Println( - "- Target serials: %s" % target_device_serials) + self.logger.Println("- Target serials: %s" % target_device_serials) # create job and add. new_job = model.JobModel() @@ -403,7 +403,8 @@ class ScheduleHandler(webapp2.RequestHandler): latest_job = latest_job_key.get() if datetime.datetime.now() - latest_job.timestamp > ( - datetime.timedelta(minutes=schedule.period)): + datetime.timedelta( + minutes=self.GetCorrectedPeriod(schedule))): ret_list.append(schedule) return ret_list @@ -490,3 +491,24 @@ class ScheduleHandler(webapp2.RequestHandler): return "" return schedule.device[0].split("/")[1].lower() + + def GetCorrectedPeriod(self, schedule): + """Corrects and returns period value based on latest children jobs. + + Args: + schedule: a model.ScheduleModel instance containing schedule + information. + + Returns: + an integer, corrected schedule period. + """ + if not schedule.error_count or not schedule.children_jobs or ( + schedule.period <= BOOTUP_ERROR_RETRY_INTERVAL_IN_MINS): + return schedule.period + + latest_job = schedule.children_jobs[-1].get() + + if latest_job.status == Status.JOB_STATUS_DICT["bootup-err"]: + return BOOTUP_ERROR_RETRY_INTERVAL_IN_MINS + else: + return schedule.period diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index c639fa6..d31fc8e 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -26,6 +26,7 @@ from webapp.src import vtslab_status as Status from webapp.src.proto import model from webapp.src.scheduler import schedule_worker from webapp.src.testing import unittest_base +from webapp.src.utils import model_util class ScheduleHandlerTest(unittest_base.UnitTestBase): @@ -294,6 +295,81 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): self.assertEquals(schedule1_l_original_priority_value - 1, schedule1_l.priority_value) + def testRetryAfterBootupError(self): + """Asserts a schedule's period is shortened after boot-up error.""" + long_period = 5760 + + lab = self.GenerateLabModel() + lab.put() + + device = self.GenerateDeviceModel(hostname=lab.hostname) + device.put() + + schedule = self.GenerateScheduleModel( + device_model=device, lab_model=lab, period=long_period) + schedule.put() + + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() + + # a job should be created. + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(1, len(jobs)) + + jobs[0].status = Status.JOB_STATUS_DICT["bootup-err"] + jobs[0].put() + model_util.UpdateParentSchedule(jobs[0], + Status.JOB_STATUS_DICT["bootup-err"]) + + self.PassTime( + minutes=schedule_worker.BOOTUP_ERROR_RETRY_INTERVAL_IN_MINS + 1) + self.ResetDevices() + + # new job should be created again. + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(2, len(jobs)) + + jobs.sort(key=lambda x: x.timestamp, reverse=True) # latest first + jobs[0].status = Status.JOB_STATUS_DICT["bootup-err"] + jobs[0].put() + model_util.UpdateParentSchedule(jobs[0], + Status.JOB_STATUS_DICT["bootup-err"]) + + self.PassTime( + minutes=schedule_worker.BOOTUP_ERROR_RETRY_INTERVAL_IN_MINS - 1) + self.ResetDevices() + + # time is not passed enough so there would be no new job. + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(2, len(jobs)) + + # if latest job is completed successfully, period should be recovered. + jobs[0].status = Status.JOB_STATUS_DICT["complete"] + jobs[0].put() + model_util.UpdateParentSchedule(jobs[0], + Status.JOB_STATUS_DICT["complete"]) + + # pass time to (period - 1) + self.PassTime(minutes=long_period - 1 - ( + schedule_worker.BOOTUP_ERROR_RETRY_INTERVAL_IN_MINS - 1)) + self.ResetDevices() + + # then no job will be created. + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(2, len(jobs)) + + # pass time to (period + 1) + self.PassTime(minutes=2) + + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(3, len(jobs)) + if __name__ == "__main__": unittest.main() diff --git a/gae/webapp/src/utils/model_util.py b/gae/webapp/src/utils/model_util.py index 66c575e..aa07a63 100644 --- a/gae/webapp/src/utils/model_util.py +++ b/gae/webapp/src/utils/model_util.py @@ -49,7 +49,7 @@ def UpdateParentSchedule(job, status): Status.JOB_STATUS_DICT["bootup-err"] ]: schedule.error_count += 1 - if schedule.error_count >= 3: + if schedule.error_count >= Status.NUM_ERRORS_FOR_SUSPENSION: schedule.suspended = True schedule.put() if previous_suspended != schedule.suspended: diff --git a/gae/webapp/src/vtslab_status.py b/gae/webapp/src/vtslab_status.py index 967bbf0..256c32d 100644 --- a/gae/webapp/src/vtslab_status.py +++ b/gae/webapp/src/vtslab_status.py @@ -98,6 +98,9 @@ TEST_TYPE_DICT = { TEST_TYPE_MANUAL: 1 << 5 } +# # of errors in a row to suspend a schedule +NUM_ERRORS_FOR_SUSPENSION = 3 + def GetPriorityValue(priority): """Helper function to sort jobs based on priority. -- cgit v1.2.3 From 2c05277bfaf3443098be73cfccf83c5faff94ed2 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Fri, 15 Jun 2018 13:47:08 +0900 Subject: Extend JOB_RESPONSE_TIMEOUT_SECONDS to 1 hour. Test: mma Bug: 72354745 Change-Id: Id2100c99a3a1f9256442d93685627163d62db442 --- gae/webapp/src/scheduler/job_heartbeat.py | 6 ++-- gae/webapp/src/scheduler/job_heartbeat_test.py | 48 +++++++++++++++++--------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/gae/webapp/src/scheduler/job_heartbeat.py b/gae/webapp/src/scheduler/job_heartbeat.py index d795714..af8994c 100644 --- a/gae/webapp/src/scheduler/job_heartbeat.py +++ b/gae/webapp/src/scheduler/job_heartbeat.py @@ -27,7 +27,7 @@ from webapp.src.utils import email_util from webapp.src.utils import logger from webapp.src.utils import model_util -JOB_RESPONSE_TIMEOUT_SECONDS = 300 +JOB_RESPONSE_TIMEOUT_SECONDS = 60 * 60 class PeriodicJobHeartBeat(webapp2.RequestHandler): @@ -66,8 +66,8 @@ class PeriodicJobHeartBeat(webapp2.RequestHandler): job.hostname, job.device, job.test_name)) job.status = Status.JOB_STATUS_DICT["infra-err"] lost_jobs_to_put.append(job) - model_util.UpdateParentSchedule(job, - Status.JOB_STATUS_DICT["infra-err"]) + model_util.UpdateParentSchedule( + job, Status.JOB_STATUS_DICT["infra-err"]) device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) diff --git a/gae/webapp/src/scheduler/job_heartbeat_test.py b/gae/webapp/src/scheduler/job_heartbeat_test.py index 01cf12c..9728ed7 100644 --- a/gae/webapp/src/scheduler/job_heartbeat_test.py +++ b/gae/webapp/src/scheduler/job_heartbeat_test.py @@ -80,30 +80,46 @@ class JobHeartbeatTest(unittest_base.UnitTestBase): scheduler.response.write = mock.Mock() scheduler.request.get = mock.MagicMock(return_value="") - print("\nCreating jobs...") + # Creating jobs. scheduler.post() jobs = model.JobModel.query().fetch() self.assertEqual(2, len(jobs)) - print("Making jobs get old and running heartbeat monitor...") - for job in jobs: - job.status = Status.JOB_STATUS_DICT["leased"] - job.timestamp = ( - datetime.datetime.now() - datetime.timedelta(minutes=10)) - job.heartbeat_stamp = ( - datetime.datetime.now() - datetime.timedelta(minutes=7)) - job.put() - + # jobs[0] will get old enough so it will be timed out. + jobs[0].status = Status.JOB_STATUS_DICT["leased"] + jobs[0].timestamp = (datetime.datetime.now() - datetime.timedelta( + seconds=job_heartbeat.JOB_RESPONSE_TIMEOUT_SECONDS + 5)) + jobs[0].heartbeat_stamp = ( + datetime.datetime.now() - datetime.timedelta( + seconds=job_heartbeat.JOB_RESPONSE_TIMEOUT_SECONDS + 5)) + jobs[0].put() + + # jobs[1] will not exceed the timeout time. + jobs[1].status = Status.JOB_STATUS_DICT["leased"] + jobs[1].timestamp = (datetime.datetime.now() - datetime.timedelta( + seconds=job_heartbeat.JOB_RESPONSE_TIMEOUT_SECONDS - 5)) + jobs[1].heartbeat_stamp = ( + datetime.datetime.now() - datetime.timedelta( + seconds=job_heartbeat.JOB_RESPONSE_TIMEOUT_SECONDS - 5)) + jobs[1].put() + + # Creating jobs. self.job_heartbeat.get() + # One job(job[0]) should be changed to infra-err status. jobs = model.JobModel.query().fetch() - for job in jobs: - self.assertEquals(Status.JOB_STATUS_DICT["infra-err"], job.status) - - devices = model.DeviceModel.query().fetch() + infra_error_jobs = [ + x for x in jobs if x.status == Status.JOB_STATUS_DICT["infra-err"] + ] + self.assertEquals(len(infra_error_jobs), 1) + + # job[0]'s devices should be changed to free scheduling status. + serials = infra_error_jobs[0].serial + devices = model.DeviceModel.query( + model.DeviceModel.serial.IN(serials)).fetch() for device in devices: - self.assertEquals(Status.DEVICE_SCHEDULING_STATUS_DICT["free"], - device.scheduling_status) + self.assertEquals(device.scheduling_status, + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) if __name__ == "__main__": -- cgit v1.2.3 From d63ad2889aba192f6f32226d41eacd91b8eaa737 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 3 Jul 2018 11:54:16 +0900 Subject: Reset error count when resuming a schedule. Test: mma Bug: 73845606 --- gae/webapp/src/dashboard/schedule_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gae/webapp/src/dashboard/schedule_list.py b/gae/webapp/src/dashboard/schedule_list.py index 50de2ad..f7266f9 100644 --- a/gae/webapp/src/dashboard/schedule_list.py +++ b/gae/webapp/src/dashboard/schedule_list.py @@ -34,6 +34,7 @@ class SchedulePage(base.BaseHandler): if resume_key: schedule_key = ndb.key.Key(urlsafe=resume_key) schedule = schedule_key.get() + schedule.error_count = 0 schedule.suspended = False schedule.put() email_util.send_schedule_suspension_notification(schedule) -- cgit v1.2.3 From 02a9e006257f9d862effb010ee01852d5e9f89fb Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 26 Jun 2018 11:15:04 +0900 Subject: Add bare angular project 'frontend'. Test: mma Bug: 74575555 Change-Id: I2819ba2479ae5bacebf5776214cc479785ad0337 --- gae/app.yaml | 1 + gae/frontend/.editorconfig | 13 + gae/frontend/.gitignore | 39 + gae/frontend/MODULE_LICENSE_MIT | 0 gae/frontend/NOTICE | 22 + gae/frontend/README.md | 27 + gae/frontend/angular.json | 127 + gae/frontend/e2e/protractor.conf.js | 28 + gae/frontend/e2e/src/app.e2e-spec.ts | 14 + gae/frontend/e2e/src/app.po.ts | 11 + gae/frontend/e2e/tsconfig.e2e.json | 13 + gae/frontend/package-lock.json | 10716 ++++++++++++++++++++ gae/frontend/package.json | 48 + gae/frontend/src/app/app.component.css | 0 gae/frontend/src/app/app.component.html | 20 + gae/frontend/src/app/app.component.spec.ts | 27 + gae/frontend/src/app/app.component.ts | 10 + gae/frontend/src/app/app.module.ts | 16 + gae/frontend/src/assets/.gitkeep | 0 gae/frontend/src/browserslist | 9 + gae/frontend/src/environments/environment.prod.ts | 3 + gae/frontend/src/environments/environment.ts | 15 + gae/frontend/src/favicon.ico | Bin 0 -> 5430 bytes gae/frontend/src/index.html | 14 + gae/frontend/src/karma.conf.js | 31 + gae/frontend/src/main.ts | 12 + gae/frontend/src/polyfills.ts | 80 + gae/frontend/src/styles.css | 1 + gae/frontend/src/test.ts | 20 + gae/frontend/src/tsconfig.app.json | 12 + gae/frontend/src/tsconfig.spec.json | 19 + gae/frontend/src/tslint.json | 17 + gae/frontend/tsconfig.json | 20 + gae/frontend/tslint.json | 130 + gae/worker.yaml | 13 + 35 files changed, 11528 insertions(+) create mode 100644 gae/frontend/.editorconfig create mode 100644 gae/frontend/.gitignore create mode 100644 gae/frontend/MODULE_LICENSE_MIT create mode 100644 gae/frontend/NOTICE create mode 100644 gae/frontend/README.md create mode 100644 gae/frontend/angular.json create mode 100644 gae/frontend/e2e/protractor.conf.js create mode 100644 gae/frontend/e2e/src/app.e2e-spec.ts create mode 100644 gae/frontend/e2e/src/app.po.ts create mode 100644 gae/frontend/e2e/tsconfig.e2e.json create mode 100644 gae/frontend/package-lock.json create mode 100644 gae/frontend/package.json create mode 100644 gae/frontend/src/app/app.component.css create mode 100644 gae/frontend/src/app/app.component.html create mode 100644 gae/frontend/src/app/app.component.spec.ts create mode 100644 gae/frontend/src/app/app.component.ts create mode 100644 gae/frontend/src/app/app.module.ts create mode 100644 gae/frontend/src/assets/.gitkeep create mode 100644 gae/frontend/src/browserslist create mode 100644 gae/frontend/src/environments/environment.prod.ts create mode 100644 gae/frontend/src/environments/environment.ts create mode 100644 gae/frontend/src/favicon.ico create mode 100644 gae/frontend/src/index.html create mode 100644 gae/frontend/src/karma.conf.js create mode 100644 gae/frontend/src/main.ts create mode 100644 gae/frontend/src/polyfills.ts create mode 100644 gae/frontend/src/styles.css create mode 100644 gae/frontend/src/test.ts create mode 100644 gae/frontend/src/tsconfig.app.json create mode 100644 gae/frontend/src/tsconfig.spec.json create mode 100644 gae/frontend/src/tslint.json create mode 100644 gae/frontend/tsconfig.json create mode 100644 gae/frontend/tslint.json diff --git a/gae/app.yaml b/gae/app.yaml index e9fce8f..1c0d3a7 100644 --- a/gae/app.yaml +++ b/gae/app.yaml @@ -52,4 +52,5 @@ skip_files: - ^script/*$ - testrunner.py - .*_test.py$ +- ^(.*/)?frontend/(.*) # [END exclude] diff --git a/gae/frontend/.editorconfig b/gae/frontend/.editorconfig new file mode 100644 index 0000000..6e87a00 --- /dev/null +++ b/gae/frontend/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/gae/frontend/.gitignore b/gae/frontend/.gitignore new file mode 100644 index 0000000..ee5c9d8 --- /dev/null +++ b/gae/frontend/.gitignore @@ -0,0 +1,39 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/gae/frontend/MODULE_LICENSE_MIT b/gae/frontend/MODULE_LICENSE_MIT new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/NOTICE b/gae/frontend/NOTICE new file mode 100644 index 0000000..40040b9 --- /dev/null +++ b/gae/frontend/NOTICE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2014-2018 Google, Inc. http://angular.io + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/gae/frontend/README.md b/gae/frontend/README.md new file mode 100644 index 0000000..4217895 --- /dev/null +++ b/gae/frontend/README.md @@ -0,0 +1,27 @@ +# Frontend + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.8. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/gae/frontend/angular.json b/gae/frontend/angular.json new file mode 100644 index 0000000..29cb325 --- /dev/null +++ b/gae/frontend/angular.json @@ -0,0 +1,127 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "frontend": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": {}, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "frontend:build" + }, + "configurations": { + "production": { + "browserTarget": "frontend:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ + "src/styles.css" + ], + "scripts": [], + "assets": [ + "src/favicon.ico", + "src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "frontend-e2e": { + "root": "e2e/", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "frontend:serve" + }, + "configurations": { + "production": { + "devServerTarget": "frontend:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "frontend" +} \ No newline at end of file diff --git a/gae/frontend/e2e/protractor.conf.js b/gae/frontend/e2e/protractor.conf.js new file mode 100644 index 0000000..86776a3 --- /dev/null +++ b/gae/frontend/e2e/protractor.conf.js @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.e2e.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; \ No newline at end of file diff --git a/gae/frontend/e2e/src/app.e2e-spec.ts b/gae/frontend/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000..87525cf --- /dev/null +++ b/gae/frontend/e2e/src/app.e2e-spec.ts @@ -0,0 +1,14 @@ +import { AppPage } from './app.po'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('Welcome to frontend!'); + }); +}); diff --git a/gae/frontend/e2e/src/app.po.ts b/gae/frontend/e2e/src/app.po.ts new file mode 100644 index 0000000..82ea75b --- /dev/null +++ b/gae/frontend/e2e/src/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/gae/frontend/e2e/tsconfig.e2e.json b/gae/frontend/e2e/tsconfig.e2e.json new file mode 100644 index 0000000..a6dd622 --- /dev/null +++ b/gae/frontend/e2e/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git a/gae/frontend/package-lock.json b/gae/frontend/package-lock.json new file mode 100644 index 0000000..ed44c54 --- /dev/null +++ b/gae/frontend/package-lock.json @@ -0,0 +1,10716 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.6.8.tgz", + "integrity": "sha512-ZKTm/zC61iY9IBHOEAKoMSzZpvhkmv+1O/HHzpHEuR551jCzu6vSyCmMY9Z7GBcccscCV+hjeSMwgFrFRcqlkw==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "rxjs": "6.2.1" + } + }, + "@angular-devkit/build-angular": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.6.8.tgz", + "integrity": "sha512-VGqYAk8jpISraz2UHfsDre270NOUmV0CTSZw2p9sm5g/XIr5m+IHetFZz3gpoAr9+If2aFTs8Rt3sGdCRzwBqA==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.6.8", + "@angular-devkit/build-optimizer": "0.6.8", + "@angular-devkit/core": "0.6.8", + "@ngtools/webpack": "6.0.8", + "ajv": "6.4.0", + "autoprefixer": "8.6.3", + "cache-loader": "1.2.2", + "chalk": "2.2.2", + "circular-dependency-plugin": "5.0.2", + "clean-css": "4.1.11", + "copy-webpack-plugin": "4.5.1", + "file-loader": "1.1.11", + "glob": "7.1.2", + "html-webpack-plugin": "3.2.0", + "istanbul": "0.4.5", + "istanbul-instrumenter-loader": "3.0.1", + "karma-source-map-support": "1.3.0", + "less": "3.0.4", + "less-loader": "4.1.0", + "license-webpack-plugin": "1.3.1", + "lodash": "4.17.10", + "memory-fs": "0.4.1", + "mini-css-extract-plugin": "0.4.0", + "minimatch": "3.0.4", + "node-sass": "4.9.0", + "opn": "5.3.0", + "parse5": "4.0.0", + "portfinder": "1.0.13", + "postcss": "6.0.23", + "postcss-import": "11.1.0", + "postcss-loader": "2.1.5", + "postcss-url": "7.3.2", + "raw-loader": "0.5.1", + "resolve": "1.8.1", + "rxjs": "6.2.1", + "sass-loader": "7.0.3", + "silent-error": "1.1.0", + "source-map-support": "0.5.6", + "stats-webpack-plugin": "0.6.2", + "style-loader": "0.21.0", + "stylus": "0.54.5", + "stylus-loader": "3.0.2", + "tree-kill": "1.2.0", + "uglifyjs-webpack-plugin": "1.2.7", + "url-loader": "1.0.1", + "webpack": "4.8.3", + "webpack-dev-middleware": "3.1.3", + "webpack-dev-server": "3.1.4", + "webpack-merge": "4.1.3", + "webpack-sources": "1.1.0", + "webpack-subresource-integrity": "1.1.0-rc.4" + } + }, + "@angular-devkit/build-optimizer": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.6.8.tgz", + "integrity": "sha512-of5syQbv3uNPp4AQkfRecfnp8AE8kvffbfYi+FFPZ6OGr7e59T1fGwk6+Zgb2qQFQg8HO2tzWI/uygtLIqmbmw==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "source-map": "0.5.7", + "typescript": "2.9.2", + "webpack-sources": "1.1.0" + }, + "dependencies": { + "typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "dev": true + } + } + }, + "@angular-devkit/core": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.6.8.tgz", + "integrity": "sha512-rkIa1OSVWTt4g9leLSK/PsqOj3HZbDKHbZjqlslyfVa3AyCeiumFoOgViOVXlYgPX3HHDbE5uH24nyUWSD8uww==", + "dev": true, + "requires": { + "ajv": "6.4.0", + "chokidar": "2.0.4", + "rxjs": "6.2.1", + "source-map": "0.5.7" + } + }, + "@angular-devkit/schematics": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.6.8.tgz", + "integrity": "sha512-R4YqAUdo62wtrhX/5HSRGSKXNTWqfQb66ZE6m8jj6GEJNFKdNXMdxOchxr07LCiKTxfh1w6G3nGzxIsu/+D4KA==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "rxjs": "6.2.1" + } + }, + "@angular/animations": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.0.6.tgz", + "integrity": "sha512-mJvWn0GuYARJfV9/KNUn5qUc5iNJKMSSNm//pRtUB8n829KnJHLnGpNsr95dzARH5wI3Om/t6hG3M0XCLbIfNQ==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/cli": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-6.0.8.tgz", + "integrity": "sha512-DhH1Zq5Yonthw6zh6W07fhf+9XrAZbD1fcQ0MrmbxlieCfLlTAdBqyK2LavFCKwSZkUMLF6UHM3+jiNRVZSSIg==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.6.8", + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "@schematics/angular": "0.6.8", + "@schematics/update": "0.6.8", + "opn": "5.3.0", + "resolve": "1.8.1", + "rxjs": "6.2.1", + "semver": "5.5.0", + "silent-error": "1.1.0", + "symbol-observable": "1.2.0", + "yargs-parser": "10.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "yargs-parser": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.0.0.tgz", + "integrity": "sha512-+DHejWujTVYeMHLff8U96rLc4uE4Emncoftvn5AjhB1Jw1pWxLzgBUT/WYbPrHmy6YPEBTZQx5myHhVcuuu64g==", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "@angular/common": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.0.6.tgz", + "integrity": "sha512-SjCrrGNJSeRMtNLv/ug5HpyRUexdNl11TrWCWMeu3ye3ss4k6EnuM9jGB196B0PIm0IbjO0KrpQ8bqBx0/2vqw==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/compiler": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.0.6.tgz", + "integrity": "sha512-lcDNfkYLOWzOOqdD2Kspxwjk3xGs8kVLbq/8uk/aJ96ty8aA9j8Nbf3h53SCY9LuGoJMjOaaUpgwZCszFzqQyA==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/compiler-cli": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-6.0.6.tgz", + "integrity": "sha512-vWJK+X6B63+kdAN2s7Az1NHF4gAbECf1fkB+zkO6pP706teW4VlN2xdXeHLXgvK39iDJbhbctTnDfhqIaPmyjw==", + "dev": true, + "requires": { + "chokidar": "1.7.0", + "minimist": "1.2.0", + "reflect-metadata": "0.1.12", + "tsickle": "0.29.0" + }, + "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.2.4", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "@angular/core": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.0.6.tgz", + "integrity": "sha512-7J4wuQ5Bss2GmCptyXSfmgWk/IbCFK/MJwaXOpADLB9iWOkOIvKRSTntb4l6j3OVd9boCbs6Z/xW/HT964iMvw==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/forms": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.0.6.tgz", + "integrity": "sha512-uVcvUz8JzO/R6HtxIUtefjK55nf4gJt9WjVdnjmA66pQe1+aQYscyQu9QFykGfGqta/0luhVSU7J+5g0rIRr/g==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/http": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.0.6.tgz", + "integrity": "sha512-ZyY7JS3lQM0HnKfoCJl+S9ZHeQVdG+FefjYE2s7pBKUufaoMo9DTIfQe5ZgSQeXRAFKjuUyJDf1EZlPVVvQzIw==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/language-service": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-6.0.6.tgz", + "integrity": "sha512-6zRuKreMPlLQkLGS7KaJ4xehwirPbst+S6tQZltcSHjgIKrZBu3acL7/tUo5G5jQW6OnPXWK9UYs2kCffPS3AQ==", + "dev": true + }, + "@angular/platform-browser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.0.6.tgz", + "integrity": "sha512-c+2c4Ba8IeIt9CnF1RmJVf/0xwljT9GSIJUC61SLrX01NMwRxDq/LC+tatcBGLzZ6rc1eYmsd1exTHOGfENOxw==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/platform-browser-dynamic": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.0.6.tgz", + "integrity": "sha512-t5+dvfcwVaDa5H8qsVnPAvmNJa0rDwJMu1T6kfz8sAxzgiw6tOvIQShJX0Ka94+nPpd4mg7gv43VV705z6ryMA==", + "requires": { + "tslib": "1.9.3" + } + }, + "@angular/router": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.0.6.tgz", + "integrity": "sha512-R49Gh/ate//AloPGjtQ2Nl3HNMT21pumcUoWZEZtYw8UyTbxSKLMc40yzdsldGrKZ/G/CafFTaS1hpZD7MF5/w==", + "requires": { + "tslib": "1.9.3" + } + }, + "@ngtools/webpack": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-6.0.8.tgz", + "integrity": "sha512-jorGpTd82ILbyUwg4JQekovHFaYwSMlZan4f7x+sd3+2WgyL3Z1+ZbVSGKvXZWKS/mAVx7eLkRikzJkuC4FgHw==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "tree-kill": "1.2.0", + "webpack-sources": "1.1.0" + } + }, + "@schematics/angular": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.6.8.tgz", + "integrity": "sha512-9kRphqTYG5Df/I8fvnT1zMsw0YNDPO9tl18tQZXj4am4raT7l9UCr+WkwJdlBoA5pwG6baWE9sL0iGWV/bzF/g==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "typescript": "2.7.2" + } + }, + "@schematics/update": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.6.8.tgz", + "integrity": "sha512-1Uq7LYnwL2wBwGVCgNz76QAR13ghAk+2vDDHOi+VX5+usHManxydrpoMGeX66OBPd+y5D3D2MFb+8mYHE7mygg==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "npm-registry-client": "8.5.1", + "rxjs": "6.2.1", + "semver": "5.5.0", + "semver-intersect": "1.3.1" + } + }, + "@types/jasmine": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", + "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==", + "dev": true + }, + "@types/jasminewd2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.3.tgz", + "integrity": "sha512-hYDVmQZT5VA2kigd4H4bv7vl/OhlympwREUemqBdOqtrYTo5Ytm12a5W5/nGgGYdanGVxj0x/VhZ7J3hOg/YKg==", + "dev": true, + "requires": { + "@types/jasmine": "2.8.8" + } + }, + "@types/node": { + "version": "8.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", + "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==", + "dev": true + }, + "@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "@types/selenium-webdriver": { + "version": "2.53.43", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.43.tgz", + "integrity": "sha512-UBYHWph6P3tutkbXpW6XYg9ZPbTKjw/YC2hGG1/GEvWwTbvezBUv3h+mmUFw79T3RFPnmedpiXdOBbXX+4l0jg==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.4.3.tgz", + "integrity": "sha512-S6npYhPcTHDYe9nlsKa9CyWByFi8Vj8HovcAgtmMAQZUOczOZbQ8CnwMYKYC5HEZzxEE+oY0jfQk4cVlI3J59Q==", + "dev": true, + "requires": { + "@webassemblyjs/helper-wasm-bytecode": "1.4.3", + "@webassemblyjs/wast-parser": "1.4.3", + "debug": "3.1.0", + "webassemblyjs": "1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.4.3.tgz", + "integrity": "sha512-3zTkSFswwZOPNHnzkP9ONq4bjJSeKVMcuahGXubrlLmZP8fmTIJ58dW7h/zOVWiFSuG2em3/HH3BlCN7wyu9Rw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.4.3.tgz", + "integrity": "sha512-e8+KZHh+RV8MUvoSRtuT1sFXskFnWG9vbDy47Oa166xX+l0dD5sERJ21g5/tcH8Yo95e9IN3u7Jc3NbhnUcSkw==", + "dev": true, + "requires": { + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.4.3.tgz", + "integrity": "sha512-9FgHEtNsZQYaKrGCtsjswBil48Qp1agrzRcPzCbQloCoaTbOXLJ9IRmqT+uEZbenpULLRNFugz3I4uw18hJM8w==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.4.3" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.4.3.tgz", + "integrity": "sha512-JINY76U+702IRf7ePukOt037RwmtH59JHvcdWbTTyHi18ixmQ+uOuNhcdCcQHTquDAH35/QgFlp3Y9KqtyJsCQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.4.3.tgz", + "integrity": "sha512-I7bS+HaO0K07Io89qhJv+z1QipTpuramGwUSDkwEaficbSvCcL92CUZEtgykfNtk5wb0CoLQwWlmXTwGbNZUeQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.4.3.tgz", + "integrity": "sha512-p0yeeO/h2r30PyjnJX9xXSR6EDcvJd/jC6xa/Pxg4lpfcNi7JUswOpqDToZQ55HMMVhXDih/yqkaywHWGLxqyQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/helper-buffer": "1.4.3", + "@webassemblyjs/helper-wasm-bytecode": "1.4.3", + "@webassemblyjs/wasm-gen": "1.4.3", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/leb128": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.4.3.tgz", + "integrity": "sha512-4u0LJLSPzuRDWHwdqsrThYn+WqMFVqbI2ltNrHvZZkzFPO8XOZ0HFQ5eVc4jY/TNHgXcnwrHjONhPGYuuf//KQ==", + "dev": true, + "requires": { + "leb": "0.3.0" + } + }, + "@webassemblyjs/validation": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/validation/-/validation-1.4.3.tgz", + "integrity": "sha512-R+rRMKfhd9mq0rj2mhU9A9NKI2l/Rw65vIYzz4lui7eTKPcCu1l7iZNi4b9Gen8D42Sqh/KGiaQNk/x5Tn/iBQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3" + } + }, + "@webassemblyjs/wasm-edit": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.4.3.tgz", + "integrity": "sha512-qzuwUn771PV6/LilqkXcS0ozJYAeY/OKbXIWU3a8gexuqb6De2p4ya/baBeH5JQ2WJdfhWhSvSbu86Vienttpw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/helper-buffer": "1.4.3", + "@webassemblyjs/helper-wasm-bytecode": "1.4.3", + "@webassemblyjs/helper-wasm-section": "1.4.3", + "@webassemblyjs/wasm-gen": "1.4.3", + "@webassemblyjs/wasm-opt": "1.4.3", + "@webassemblyjs/wasm-parser": "1.4.3", + "@webassemblyjs/wast-printer": "1.4.3", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.4.3.tgz", + "integrity": "sha512-eR394T8dHZfpLJ7U/Z5pFSvxl1L63JdREebpv9gYc55zLhzzdJPAuxjBYT4XqevUdW67qU2s0nNA3kBuNJHbaQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/helper-wasm-bytecode": "1.4.3", + "@webassemblyjs/leb128": "1.4.3" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.4.3.tgz", + "integrity": "sha512-7Gp+nschuKiDuAL1xmp4Xz0rgEbxioFXw4nCFYEmy+ytynhBnTeGc9W9cB1XRu1w8pqRU2lbj2VBBA4cL5Z2Kw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/helper-buffer": "1.4.3", + "@webassemblyjs/wasm-gen": "1.4.3", + "@webassemblyjs/wasm-parser": "1.4.3", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.4.3.tgz", + "integrity": "sha512-KXBjtlwA3BVukR/yWHC9GF+SCzBcgj0a7lm92kTOaa4cbjaTaa47bCjXw6cX4SGQpkncB9PU2hHGYVyyI7wFRg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/helper-wasm-bytecode": "1.4.3", + "@webassemblyjs/leb128": "1.4.3", + "@webassemblyjs/wasm-parser": "1.4.3", + "webassemblyjs": "1.4.3" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.4.3.tgz", + "integrity": "sha512-QhCsQzqV0CpsEkRYyTzQDilCNUZ+5j92f+g35bHHNqS22FppNTywNFfHPq8ZWZfYCgbectc+PoghD+xfzVFh1Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/floating-point-hex-parser": "1.4.3", + "@webassemblyjs/helper-code-frame": "1.4.3", + "@webassemblyjs/helper-fsm": "1.4.3", + "long": "3.2.0", + "webassemblyjs": "1.4.3" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.4.3.tgz", + "integrity": "sha512-EgXk4anf8jKmuZJsqD8qy5bz2frEQhBvZruv+bqwNoLWUItjNSFygk8ywL3JTEz9KtxTlAmqTXNrdD1d9gNDtg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/wast-parser": "1.4.3", + "long": "3.2.0" + } + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "dev": true, + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "dev": true, + "requires": { + "acorn": "5.7.1" + } + }, + "adm-zip": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz", + "integrity": "sha1-ph7VrmkFw66lizplfSUDMJEFJzY=", + "dev": true + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "dev": true, + "requires": { + "es6-promisify": "5.0.0" + } + }, + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", + "dev": true, + "requires": { + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1", + "uri-js": "3.0.2" + } + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.2" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + } + }, + "app-root-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz", + "integrity": "sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=", + "dev": true + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "2.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", + "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=", + "dev": true + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.12.0" + } + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, + "autoprefixer": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.3.tgz", + "integrity": "sha512-KkQyCHBxma7R2eoEkjja/RHUBw+Fc1nY46LdV62fzJI5D7i8mLLCtAZ/AVR3UbXhDZ8mUz4C/PF4lZrbiHa1ZQ==", + "dev": true, + "requires": { + "browserslist": "3.2.8", + "caniuse-lite": "1.0.30000858", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "6.0.23", + "postcss-value-parser": "3.3.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.10", + "source-map": "0.5.7", + "trim-right": "1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.7", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.10" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.10" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.10", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "optional": true, + "requires": { + "inherits": "2.0.3" + } + }, + "blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "dev": true, + "requires": { + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.3", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.16" + }, + "dependencies": { + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "2.1.1", + "deep-equal": "1.0.1", + "dns-equal": "1.0.0", + "dns-txt": "2.0.2", + "multicast-dns": "6.2.3", + "multicast-dns-service-types": "1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "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.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "1.2.0", + "browserify-des": "1.0.1", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", + "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.1" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "1.0.6" + } + }, + "browserslist": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", + "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", + "dev": true, + "requires": { + "caniuse-lite": "1.0.30000858", + "electron-to-chromium": "1.3.50" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "1.3.0", + "ieee754": "1.1.12", + "isarray": "1.0.0" + } + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "chownr": "1.0.1", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "lru-cache": "4.1.3", + "mississippi": "2.0.0", + "mkdirp": "0.5.1", + "move-concurrently": "1.0.1", + "promise-inflight": "1.0.1", + "rimraf": "2.6.2", + "ssri": "5.3.0", + "unique-filename": "1.1.0", + "y18n": "4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "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" + } + }, + "cache-loader": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-1.2.2.tgz", + "integrity": "sha512-rsGh4SIYyB9glU+d0OcHwiXHXBoUgDhHZaQ1KAbiXqfz1CDPxtTboh1gPbJ0q2qdO8a9lfcjgC5CJ2Ms32y5bw==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "mkdirp": "0.5.1", + "neo-async": "2.5.1", + "schema-utils": "0.4.5" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30000858", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000858.tgz", + "integrity": "sha512-oJRGfVfwHr0VKcoy2UqIoRmQcDOugnNAQsWYI3/JTzExrlzxSKtmLW1N4h+gmjgpYCEJthHmaIjok894H5il/g==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz", + "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "dev": true, + "requires": { + "anymatch": "2.0.0", + "async-each": "1.0.1", + "braces": "2.3.2", + "fsevents": "1.2.4", + "glob-parent": "3.1.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "4.0.0", + "lodash.debounce": "4.0.8", + "normalize-path": "2.1.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0", + "upath": "1.1.0" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true + }, + "chrome-trace-event": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-0.1.3.tgz", + "integrity": "sha512-sjndyZHrrWiu4RY7AkHgjn80GfAM2ZSzUkZLV/Js59Ldmh6JDThf0SUmOHU53rFu2rVxxfCzJ30Ukcfch3Gb/A==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "circular-dependency-plugin": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz", + "integrity": "sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "dev": true, + "requires": { + "for-own": "1.0.0", + "is-plain-object": "2.0.4", + "kind-of": "6.0.2", + "shallow-clone": "1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codelyzer": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.2.1.tgz", + "integrity": "sha512-CKwfgpfkqi9dyzy4s6ELaxJ54QgJ6A8iTSsM4bzHbLuTpbKncvNc3DUlCvpnkHBhK47gEf4qFsWoYqLrJPhy6g==", + "dev": true, + "requires": { + "app-root-path": "2.1.0", + "css-selector-tokenizer": "0.7.0", + "cssauron": "1.4.0", + "semver-dsl": "1.0.1", + "source-map": "0.5.7", + "sprintf-js": "1.0.3" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combine-lists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", + "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", + "dev": true, + "requires": { + "lodash": "4.17.10" + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-versions": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", + "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "compressible": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", + "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", + "dev": true, + "requires": { + "mime-db": "1.34.0" + }, + "dependencies": { + "mime-db": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.34.0.tgz", + "integrity": "sha1-RS0Oz/XDA0am3B5kseruDTcZ/5o=", + "dev": true + } + } + }, + "compression": { + "version": "1.7.2", + "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", + "dev": true, + "requires": { + "accepts": "1.3.5", + "bytes": "3.0.0", + "compressible": "2.0.14", + "debug": "2.6.9", + "on-headers": "1.0.1", + "safe-buffer": "5.1.1", + "vary": "1.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "1.1.0", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "typedarray": "0.0.6" + } + }, + "connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "1.3.2", + "utils-merge": "1.0.1" + }, + "dependencies": { + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + } + } + }, + "connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "1.2.0", + "fs-write-stream-atomic": "1.0.10", + "iferr": "0.1.5", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz", + "integrity": "sha512-OlTo6DYg0XfTKOF8eLf79wcHm4Ut10xU2cRBRPMW/NA5F9VMjZGTfRHWDIYC3s+1kObGYrBLshXWU1K0hILkNQ==", + "dev": true, + "requires": { + "cacache": "10.0.4", + "find-cache-dir": "1.0.0", + "globby": "7.1.1", + "is-glob": "4.0.0", + "loader-utils": "1.1.0", + "minimatch": "3.0.4", + "p-limit": "1.3.0", + "serialize-javascript": "1.5.0" + } + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", + "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.12.0", + "minimist": "1.2.0", + "object-assign": "4.1.1", + "os-homedir": "1.0.2", + "parse-json": "2.2.0", + "require-from-string": "1.2.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "md5.js": "1.3.4", + "ripemd160": "2.0.2", + "sha.js": "2.4.11" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "inherits": "2.0.3", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "4.1.3", + "which": "1.3.1" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "1.0.1", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.3", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "diffie-hellman": "5.0.3", + "inherits": "2.0.3", + "pbkdf2": "3.0.16", + "public-encrypt": "4.0.2", + "randombytes": "2.0.6", + "randomfill": "1.0.4" + } + }, + "css-parse": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz", + "integrity": "sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "dev": true, + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.1", + "regexpu-core": "1.0.0" + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.45" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "6.1.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.1", + "p-map": "1.2.0", + "pify": "3.0.0", + "rimraf": "2.6.2" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "detect-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", + "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } + }, + "dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "path-type": "3.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "1.1.5", + "safe-buffer": "5.1.2" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "1.1.1" + } + }, + "dom-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "dev": true, + "requires": { + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "1.0.1", + "ent": "2.2.0", + "extend": "3.0.1", + "void-elements": "2.0.1" + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", + "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "stream-shift": "1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.50", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.50.tgz", + "integrity": "sha1-dDi3b5K0G5GfP73TUPvQdX2s3fc=", + "dev": true + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.4", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "engine.io": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", + "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=", + "dev": true, + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "dev": true, + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-client": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz", + "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "1.1.2", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, + "enhanced-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz", + "integrity": "sha512-jox/62b2GofV1qTUQTMPEJSDIGycS43evqYzD/KVtEb9OCoki9cnacUPxCrZa7JfPzZSYOCZhu9O9luaMxAX8g==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "tapable": "1.0.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.3", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es5-ext": { + "version": "0.10.45", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", + "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "next-tick": "1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.45", + "es6-symbol": "3.1.1" + } + }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "4.2.4" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.45" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "4.2.1", + "estraverse": "4.2.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "4.2.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "eventsource": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", + "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "dev": true, + "requires": { + "original": "1.0.1" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.2" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.3", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "requires": { + "array-slice": "0.2.3", + "array-unique": "0.2.1", + "braces": "0.1.5" + }, + "dependencies": { + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "requires": { + "expand-range": "0.1.1" + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "requires": { + "is-number": "0.1.1", + "repeat-string": "0.2.2" + } + }, + "is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + }, + "repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.4" + }, + "dependencies": { + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "3.0.0", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "express": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "dev": true, + "requires": { + "accepts": "1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.3", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "1.4.0", + "type-is": "1.6.16", + "utils-merge": "1.0.1", + "vary": "1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "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.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + }, + "file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.4.5" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "7.1.2", + "minimatch": "3.0.4" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "unpipe": "1.0.0" + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "make-dir": "1.3.0", + "pkg-dir": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "flush-write-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "follow-redirects": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.0.tgz", + "integrity": "sha512-fdrt472/9qQ6Kgjvb935ig6vJCuofpBUD14f9Vb+SLlm7xIe4Qva5gey8EKtv8lp7ahE1wilg3xL1znpVGtZIA==", + "dev": true, + "requires": { + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "1.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "iferr": "0.1.5", + "imurmurhash": "0.1.4", + "readable-stream": "2.3.6" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "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.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.3" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "optional": true, + "requires": { + "globule": "1.2.1" + } + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true, + "optional": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "optional": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "dir-glob": "2.0.0", + "glob": "7.1.2", + "ignore": "3.3.10", + "pify": "3.0.0", + "slash": "1.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.10", + "minimatch": "3.0.4" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "handle-thing": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", + "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "hash.js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.4.tgz", + "integrity": "sha512-A6RlQvvZEtFS5fLU43IDu0QUmBy+fDO9VMdTXvufKwIkt/rFfvICAViCax5fbDO4zdNzaC3/27ZhKUok5bAJyw==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "1.1.4", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "hosted-git-info": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.1.tgz", + "integrity": "sha512-Ba4+0M4YvIDUUsprMjhVTU1yN9F2/LJSAl69ZpzaLT4l4j5mwTS6jqqW9Ojvj6lKz/veqPzpJBqGbXspOb533A==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "obuf": "1.1.2", + "readable-stream": "2.3.6", + "wbuf": "1.7.3" + } + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true + }, + "html-minifier": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.17.tgz", + "integrity": "sha512-O+StuKL0UWfwX5Zv4rFxd60DPcT5DVjGq1AlnP6VQ8wzudft/W4hx5Wl98aSYNwFBHY6XWJreRw/BehX4l+diQ==", + "dev": true, + "requires": { + "camel-case": "3.0.0", + "clean-css": "4.1.11", + "commander": "2.15.1", + "he": "1.1.1", + "param-case": "2.1.1", + "relateurl": "0.2.7", + "uglify-js": "3.4.2" + } + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "3.5.17", + "loader-utils": "0.2.17", + "lodash": "4.17.10", + "pretty-error": "2.1.1", + "tapable": "1.0.0", + "toposort": "1.0.7", + "util.promisify": "1.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + } + } + }, + "htmlparser2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.1.0", + "domutils": "1.1.6", + "readable-stream": "1.0.34" + }, + "dependencies": { + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": "1.4.0" + } + }, + "http-parser-js": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", + "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", + "dev": true + }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "dev": true, + "requires": { + "eventemitter3": "3.1.0", + "follow-redirects": "1.5.0", + "requires-port": "1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", + "dev": true, + "requires": { + "http-proxy": "1.17.0", + "is-glob": "4.0.0", + "lodash": "4.17.10", + "micromatch": "3.1.10" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.2" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "dev": true, + "requires": { + "agent-base": "4.2.0", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "import-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", + "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", + "dev": true, + "requires": { + "pkg-dir": "2.0.0", + "resolve-cwd": "2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true, + "optional": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "internal-ip": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz", + "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=", + "dev": true, + "requires": { + "meow": "3.7.0" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true, + "optional": true + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "dev": true, + "optional": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "is-my-ip-valid": "1.0.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true, + "optional": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.0.11", + "js-yaml": "3.12.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.3.1", + "wordwrap": "1.0.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-api": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", + "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "dev": true, + "requires": { + "async": "2.6.1", + "compare-versions": "3.3.0", + "fileset": "2.0.3", + "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-hook": "1.2.1", + "istanbul-lib-instrument": "1.10.1", + "istanbul-lib-report": "1.1.4", + "istanbul-lib-source-maps": "1.2.5", + "istanbul-reports": "1.3.0", + "js-yaml": "3.12.0", + "mkdirp": "0.5.1", + "once": "1.4.0" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "4.17.10" + } + } + } + }, + "istanbul-instrumenter-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", + "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", + "dev": true, + "requires": { + "convert-source-map": "1.5.1", + "istanbul-lib-instrument": "1.10.1", + "loader-utils": "1.1.0", + "schema-utils": "0.3.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "dev": true, + "requires": { + "ajv": "5.5.2" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", + "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", + "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", + "dev": true, + "requires": { + "append-transform": "1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", + "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "dev": true, + "requires": { + "babel-generator": "6.26.1", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.2.0", + "semver": "5.5.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", + "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", + "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", + "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "7.1.2", + "jasmine-core": "2.8.0" + }, + "dependencies": { + "jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "dev": true + } + } + }, + "jasmine-core": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", + "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "dev": true + }, + "js-base64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", + "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==", + "dev": true, + "optional": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "optional": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jszip": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz", + "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==", + "dev": true, + "requires": { + "core-js": "2.3.0", + "es6-promise": "3.0.2", + "lie": "3.1.1", + "pako": "1.0.6", + "readable-stream": "2.0.6" + }, + "dependencies": { + "core-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", + "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", + "dev": true + }, + "es6-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", + "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "karma": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", + "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "body-parser": "1.18.2", + "chokidar": "1.7.0", + "colors": "1.1.2", + "combine-lists": "1.0.1", + "connect": "3.6.6", + "core-js": "2.5.7", + "di": "0.0.1", + "dom-serialize": "2.2.1", + "expand-braces": "0.1.2", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "http-proxy": "1.17.0", + "isbinaryfile": "3.0.2", + "lodash": "3.10.1", + "log4js": "0.6.38", + "mime": "1.6.0", + "minimatch": "3.0.4", + "optimist": "0.6.1", + "qjobs": "1.2.0", + "range-parser": "1.2.0", + "rimraf": "2.6.2", + "safe-buffer": "5.1.2", + "socket.io": "1.7.3", + "source-map": "0.5.7", + "tmp": "0.0.31", + "useragent": "2.3.0" + }, + "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.2.4", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + } + } + }, + "karma-chrome-launcher": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", + "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "dev": true, + "requires": { + "fs-access": "1.0.1", + "which": "1.3.1" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.1.tgz", + "integrity": "sha512-UcgrHkFehI5+ivMouD8NH/UOHiX4oCAtwaANylzPFdcAuD52fnCUuelacq2gh8tZ4ydhU3+xiXofSq7j5Ehygw==", + "dev": true, + "requires": { + "istanbul-api": "1.3.1", + "minimatch": "3.0.4" + } + }, + "karma-jasmine": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", + "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=", + "dev": true + }, + "karma-jasmine-html-reporter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", + "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", + "dev": true, + "requires": { + "karma-jasmine": "1.1.2" + } + }, + "karma-source-map-support": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz", + "integrity": "sha512-HcPqdAusNez/ywa+biN4EphGz62MmQyPggUsDfsHqa7tSe4jdsxgvTKuDfIazjL+IOxpVWyT7Pr4dhAV+sxX5Q==", + "dev": true, + "requires": { + "source-map-support": "0.5.6" + } + }, + "killable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", + "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "leb": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/leb/-/leb-0.3.0.tgz", + "integrity": "sha1-Mr7p+tFoMo1q6oUi2DP0GA7tHaM=", + "dev": true + }, + "less": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/less/-/less-3.0.4.tgz", + "integrity": "sha512-q3SyEnPKbk9zh4l36PGeW2fgynKu+FpbhiUNx/yaiBUQ3V0CbACCgb9FzYWcRgI2DJlP6eI4jc8XPrCTi55YcQ==", + "dev": true, + "requires": { + "errno": "0.1.7", + "graceful-fs": "4.1.11", + "image-size": "0.5.5", + "mime": "1.6.0", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.87.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "less-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.1.0.tgz", + "integrity": "sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==", + "dev": true, + "requires": { + "clone": "2.1.1", + "loader-utils": "1.1.0", + "pify": "3.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "license-webpack-plugin": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.3.1.tgz", + "integrity": "sha512-NqAFodJdpBUuf1iD+Ij8hQvF0rCFKlO2KaieoQzAPhFgzLCtJnC7Z7x5gQbGNjoe++wOKAtAmwVEIBLqq2Yp1A==", + "dev": true, + "requires": { + "ejs": "2.6.1" + } + }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dev": true, + "requires": { + "immediate": "3.0.6" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true, + "optional": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", + "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", + "dev": true, + "optional": true + }, + "lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "2.2.2" + } + }, + "log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "semver": "4.3.6" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "loglevel": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", + "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=", + "dev": true + }, + "loglevelnext": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz", + "integrity": "sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==", + "dev": true, + "requires": { + "es6-symbol": "3.1.1", + "object.assign": "4.1.0" + } + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.7", + "readable-stream": "2.3.6" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "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.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.0.tgz", + "integrity": "sha512-2Zik6PhUZ/MbiboG6SDS9UTPL4XXy4qnyGjSdCIWRrr8xb6PwLtHE+AYOjkXJWdF0OG8vo/yrJ8CgS5WbMpzIg==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "webpack-sources": "1.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "duplexify": "3.6.0", + "end-of-stream": "1.4.1", + "flush-write-stream": "1.0.3", + "from2": "2.3.0", + "parallel-transform": "1.1.0", + "pump": "2.0.1", + "pumpify": "1.5.1", + "stream-each": "1.2.2", + "through2": "2.0.3" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "requires": { + "for-in": "0.1.8", + "is-extendable": "0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "copy-concurrently": "1.0.5", + "fs-write-stream-atomic": "1.0.10", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "1.3.1", + "thunky": "1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "requires": { + "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-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "neo-async": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.1.tgz", + "integrity": "sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "node-forge": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==", + "dev": true + }, + "node-gyp": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz", + "integrity": "sha512-qDQE/Ft9xXP6zphwx4sD0t+VhwV7yFaloMpfbL2QnnDZcyaiakWlLdtFGGQfTAwpFHdpbRhRxVhIHN1OKAjgbg==", + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.5", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.3.0", + "tar": "2.2.1", + "which": "1.3.1" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.14.2" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.2", + "stringstream": "0.0.6", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true + } + } + }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.2.0", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "domain-browser": "1.2.0", + "events": "1.1.1", + "https-browserify": "1.0.0", + "os-browserify": "0.3.0", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.6", + "stream-browserify": "2.0.1", + "stream-http": "2.8.3", + "string_decoder": "1.1.1", + "timers-browserify": "2.0.10", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.4", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "node-sass": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", + "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==", + "dev": true, + "optional": true, + "requires": { + "async-foreach": "0.1.3", + "chalk": "1.1.3", + "cross-spawn": "3.0.1", + "gaze": "1.1.3", + "get-stdin": "4.0.1", + "glob": "7.1.2", + "in-publish": "2.0.0", + "lodash.assign": "4.2.0", + "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.1", + "meow": "3.7.0", + "mkdirp": "0.5.1", + "nan": "2.10.0", + "node-gyp": "3.7.0", + "npmlog": "4.1.2", + "request": "2.79.0", + "sass-graph": "2.2.4", + "stdout-stream": "1.4.0", + "true-case-path": "1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true, + "optional": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "optional": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.15.1", + "is-my-json-valid": "2.17.2", + "pinkie-promise": "2.0.1" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.14.2" + } + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true, + "optional": true + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.11.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "qs": "6.3.2", + "stringstream": "0.0.6", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.4.3", + "uuid": "3.2.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true, + "optional": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.6.1", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "npm-package-arg": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz", + "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==", + "dev": true, + "requires": { + "hosted-git-info": "2.6.1", + "osenv": "0.1.5", + "semver": "5.5.0", + "validate-npm-package-name": "3.0.0" + } + }, + "npm-registry-client": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.5.1.tgz", + "integrity": "sha512-7rjGF2eA7hKDidGyEWmHTiKfXkbrcQAsGL/Rh4Rt3x3YNRNHhwaTzVJfW3aNvvlhg4G62VCluif0sLCb/i51Hg==", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "graceful-fs": "4.1.11", + "normalize-package-data": "2.4.0", + "npm-package-arg": "6.1.0", + "npmlog": "4.1.2", + "once": "1.4.0", + "request": "2.87.0", + "retry": "0.10.1", + "safe-buffer": "5.1.2", + "semver": "5.5.0", + "slide": "1.1.6", + "ssri": "5.3.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "1.1.5", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true, + "requires": { + "boolbase": "1.0.0" + } + }, + "null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", + "dev": true + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "has-symbols": "1.0.0", + "object-keys": "1.0.12" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.12.0" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, + "requires": { + "is-wsl": "1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "original": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.1.tgz", + "integrity": "sha512-IEvtB5vM5ULvwnqMxWBLxkS13JIEXbakizMSo3yoPNPCIWzg8TG3Usn/UhXoZFM/m+FuEA20KdzPSFq/0rS+UA==", + "dev": true, + "requires": { + "url-parse": "1.4.1" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "optional": true, + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.3.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "0.2.2", + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "dev": true, + "requires": { + "asn1.js": "4.10.1", + "browserify-aes": "1.2.0", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.16" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.2" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "pbkdf2": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", + "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", + "dev": true, + "requires": { + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + }, + "portfinder": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", + "integrity": "sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=", + "dev": true, + "requires": { + "async": "1.5.2", + "debug": "2.6.9", + "mkdirp": "0.5.1" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "source-map": "0.6.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-import": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz", + "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==", + "dev": true, + "requires": { + "postcss": "6.0.23", + "postcss-value-parser": "3.3.0", + "read-cache": "1.0.0", + "resolve": "1.8.1" + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1", + "postcss-load-options": "1.2.0", + "postcss-load-plugins": "2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-loader": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.5.tgz", + "integrity": "sha512-pV7kB5neJ0/1tZ8L1uGOBNTVBCSCXQoIsZMsrwvO8V2rKGa2tBl/f80GGVxow2jJnRJ2w1ocx693EKhZAb9Isg==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "postcss": "6.0.23", + "postcss-load-config": "1.2.0", + "schema-utils": "0.4.5" + } + }, + "postcss-url": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.2.tgz", + "integrity": "sha512-QMV5mA+pCYZQcUEPQkmor9vcPQ2MT+Ipuu8qdi1gVxbNiIiErEGft+eny1ak19qALoBkccS5AHaCaCDzh7b9MA==", + "dev": true, + "requires": { + "mime": "1.6.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "postcss": "6.0.23", + "xxhashjs": "0.2.2" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "2.0.1", + "utila": "0.4.0" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "2.0.6" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "protractor": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.3.2.tgz", + "integrity": "sha512-pw4uwwiy5lHZjIguxNpkEwJJa7hVz+bJsvaTI+IbXlfn2qXwzbF8eghW/RmrZwE2sGx82I8etb8lVjQ+JrjejA==", + "dev": true, + "requires": { + "@types/node": "6.0.113", + "@types/q": "0.0.32", + "@types/selenium-webdriver": "2.53.43", + "blocking-proxy": "1.0.1", + "chalk": "1.1.3", + "glob": "7.1.2", + "jasmine": "2.8.0", + "jasminewd2": "2.2.0", + "optimist": "0.6.1", + "q": "1.4.1", + "saucelabs": "1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "0.4.18", + "webdriver-js-extender": "1.0.0", + "webdriver-manager": "12.0.6" + }, + "dependencies": { + "@types/node": { + "version": "6.0.113", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.113.tgz", + "integrity": "sha512-f9XXUWFqryzjkZA1EqFvJHSFyqyasV17fq8zCDIzbRV4ctL7RrJGKvG+lcex86Rjbzd1GrER9h9VmF5sSjV0BQ==", + "dev": true + }, + "adm-zip": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz", + "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.1", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "webdriver-manager": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", + "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", + "dev": true, + "requires": { + "adm-zip": "0.4.11", + "chalk": "1.1.3", + "del": "2.2.2", + "glob": "7.1.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "q": "1.4.1", + "request": "2.87.0", + "rimraf": "2.6.2", + "semver": "5.5.0", + "xml2js": "0.4.19" + } + } + } + }, + "proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "dev": true, + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", + "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "parse-asn1": "5.1.1", + "randombytes": "2.0.6" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "3.6.0", + "inherits": "2.0.3", + "pump": "2.0.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", + "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==", + "dev": true + }, + "randomatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "dev": true, + "requires": { + "is-number": "4.0.0", + "kind-of": "6.0.2", + "math-random": "1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.2" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.4.0" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } + }, + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + }, + "dependencies": { + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.6", + "set-immediate-shim": "1.0.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", + "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "1.4.0", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "dev": true, + "requires": { + "css-select": "1.2.0", + "dom-converter": "0.1.4", + "htmlparser2": "3.3.0", + "strip-ansi": "3.0.1", + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "1.2.0" + } + }, + "rxjs": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz", + "integrity": "sha512-OwMxHxmnmHTUpgO+V7dZChf3Tixf4ih95cmXjzzadULziVl/FKhHScGLj4goEw9weePVOH2Q0+GcCBUhKCZc/g==", + "requires": { + "tslib": "1.9.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.10", + "scss-tokenizer": "0.2.3", + "yargs": "7.1.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true, + "optional": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true, + "optional": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + } + } + } + }, + "sass-loader": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.0.3.tgz", + "integrity": "sha512-iaSFtQcGo4SSgDw5Aes5p4VTrA5jCGSA7sGmhPIcOloBlgI1VktM2MUrk2IHHjbNagckXlPz+HWq1vAAPrcYxA==", + "dev": true, + "requires": { + "clone-deep": "2.0.2", + "loader-utils": "1.1.0", + "lodash.tail": "4.1.1", + "neo-async": "2.5.1", + "pify": "3.0.0" + } + }, + "saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "dev": true, + "requires": { + "https-proxy-agent": "2.2.1" + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=", + "dev": true + }, + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", + "dev": true, + "requires": { + "ajv": "6.4.0", + "ajv-keywords": "3.2.0" + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "optional": true, + "requires": { + "js-base64": "2.4.5", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "dev": true, + "requires": { + "jszip": "3.1.5", + "rimraf": "2.6.2", + "tmp": "0.0.30", + "xml2js": "0.4.19" + }, + "dependencies": { + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + } + } + }, + "selfsigned": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz", + "integrity": "sha512-vmZenZ+8Al3NLHkWnhBQ0x6BkML1eCP2xEi3JE+f3D9wW9fipD9NNJHYtE9XJM4TsPaHGZJIamrSI6MTg1dU2Q==", + "dev": true, + "requires": { + "node-forge": "0.7.5" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "requires": { + "semver": "5.5.0" + } + }, + "semver-intersect": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.3.1.tgz", + "integrity": "sha1-j6hKnhAovSOeRTDRo+GB5pjYhLo=", + "dev": true, + "requires": { + "semver": "5.5.0" + } + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, + "requires": { + "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.6.3", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.4.0" + }, + "dependencies": { + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz", + "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "1.3.5", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "1.0.3", + "http-errors": "1.6.3", + "mime-types": "2.1.18", + "parseurl": "1.3.2" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.2" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "dev": true, + "requires": { + "is-extendable": "0.1.1", + "kind-of": "5.1.0", + "mixin-object": "2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "silent-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/silent-error/-/silent-error-1.1.0.tgz", + "integrity": "sha1-IglwbxyFCp8dENDYQJGLRvJuG8k=", + "dev": true, + "requires": { + "debug": "2.6.9" + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.2", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "socket.io": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", + "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=", + "dev": true, + "requires": { + "debug": "2.3.3", + "engine.io": "1.8.3", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + } + } + }, + "socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "requires": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz", + "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=", + "dev": true, + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "1.8.3", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "requires": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "dev": true, + "requires": { + "faye-websocket": "0.10.0", + "uuid": "3.2.1" + } + }, + "sockjs-client": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", + "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "eventsource": "0.1.6", + "faye-websocket": "0.11.1", + "inherits": "2.0.3", + "json3": "3.3.2", + "url-parse": "1.4.1" + }, + "dependencies": { + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + } + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "dev": true, + "requires": { + "buffer-from": "1.1.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "dev": true, + "requires": { + "debug": "2.6.9", + "handle-thing": "1.2.5", + "http-deceiver": "1.2.7", + "safe-buffer": "5.1.2", + "select-hose": "2.0.0", + "spdy-transport": "2.1.0" + } + }, + "spdy-transport": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz", + "integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==", + "dev": true, + "requires": { + "debug": "2.6.9", + "detect-node": "2.0.3", + "hpack.js": "2.1.6", + "obuf": "1.1.2", + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2", + "wbuf": "1.7.3" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" + } + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "stats-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/stats-webpack-plugin/-/stats-webpack-plugin-0.6.2.tgz", + "integrity": "sha1-LFlJtTHgf4eojm6k3PrFOqjHWis=", + "dev": true, + "requires": { + "lodash": "4.17.10" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + }, + "stdout-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", + "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "dev": true, + "optional": true, + "requires": { + "readable-stream": "2.3.6" + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "stream-each": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", + "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "stream-shift": "1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "style-loader": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz", + "integrity": "sha512-T+UNsAcl3Yg+BsPKs1vd22Fr8sVT+CJMtzqc6LEw9bbJZb43lm9GoeIfUcDEefBSWC0BhYbcdupV1GtI4DGzxg==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.4.5" + } + }, + "stylus": { + "version": "0.54.5", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz", + "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=", + "dev": true, + "requires": { + "css-parse": "1.7.0", + "debug": "2.6.9", + "glob": "7.0.6", + "mkdirp": "0.5.1", + "sax": "0.5.8", + "source-map": "0.1.43" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "stylus-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", + "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "lodash.clonedeep": "4.5.0", + "when": "3.6.4" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", + "dev": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "optional": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.6", + "xtend": "4.0.1" + } + }, + "thunky": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz", + "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "tree-kill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", + "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", + "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "dev": true, + "optional": true, + "requires": { + "glob": "6.0.4" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "optional": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "ts-node": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.1.tgz", + "integrity": "sha512-XK7QmDcNHVmZkVtkiwNDWiERRHPyU8nBqZB1+iv2UhOG0q3RQ9HsZ2CMqISlFbxjrYFGfG2mX7bW4dAyxBVzUw==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "chalk": "2.4.1", + "diff": "3.5.0", + "make-error": "1.3.4", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.5.6", + "yn": "2.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "tsickle": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.29.0.tgz", + "integrity": "sha512-JpID0Lv8/irRtPmqJJxb5fCwfZhjZeKmav9Zna7UjqVuJoSbI49Wue/c2PPybX1SbRrjl7bbI/JsCl0dSUJygA==", + "dev": true, + "requires": { + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map": "0.6.1", + "source-map-support": "0.5.6" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.4.1", + "commander": "2.15.1", + "diff": "3.5.0", + "glob": "7.1.2", + "js-yaml": "3.12.0", + "minimatch": "3.0.4", + "resolve": "1.8.1", + "semver": "5.5.0", + "tslib": "1.9.3", + "tsutils": "2.27.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + } + } + }, + "tsutils": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "dev": true, + "requires": { + "tslib": "1.9.3" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", + "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", + "dev": true + }, + "uglify-js": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.2.tgz", + "integrity": "sha512-/kVQDzwiE9Vy7Y63eMkMozF4jIt0C2+xHctF9YpqNWdE/NLOuMurshkpoYGUlAbeYhACPv0HJPIHJul0Ak4/uw==", + "dev": true, + "requires": { + "commander": "2.15.1", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz", + "integrity": "sha512-1VicfKhCYHLS8m1DCApqBhoulnASsEoJ/BvpUpP4zoNAPpKzdH+ghk0olGJMmwX2/jprK2j3hAHdUbczBSy2FA==", + "dev": true, + "requires": { + "cacache": "10.0.4", + "find-cache-dir": "1.0.0", + "schema-utils": "0.4.5", + "serialize-javascript": "1.5.0", + "source-map": "0.6.1", + "uglify-es": "3.3.9", + "webpack-sources": "1.1.0", + "worker-farm": "1.6.0" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "dev": true, + "requires": { + "commander": "2.13.0", + "source-map": "0.6.1" + } + } + } + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unique-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", + "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", + "dev": true, + "requires": { + "unique-slug": "2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "dev": true, + "requires": { + "imurmurhash": "0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", + "dev": true, + "requires": { + "punycode": "2.1.1" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=", + "dev": true + }, + "url-loader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.0.1.tgz", + "integrity": "sha512-rAonpHy7231fmweBKUFe0bYnlGDty77E+fm53NZdij7j/YOpyGzc7ttqG1nAXl3aRs0k41o0PC3TvGXQiw2Zvw==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "mime": "2.3.1", + "schema-utils": "0.4.5" + }, + "dependencies": { + "mime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "dev": true + } + } + }, + "url-parse": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.1.tgz", + "integrity": "sha512-x95Td74QcvICAA0+qERaVkRpTGKyBHHYdwL2LXZm5t/gBtCB9KQSO/0zQgSTYEV1p0WcvSg79TLNPSvd5IDJMQ==", + "dev": true, + "requires": { + "querystringify": "2.0.0", + "requires-port": "1.0.0" + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "requires": { + "lru-cache": "4.1.3", + "tmp": "0.0.31" + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "object.getownpropertydescriptors": "2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "1.0.3" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "2.0.4", + "graceful-fs": "4.1.11", + "neo-async": "2.5.1" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "1.0.1" + } + }, + "webassemblyjs": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webassemblyjs/-/webassemblyjs-1.4.3.tgz", + "integrity": "sha512-4lOV1Lv6olz0PJkDGQEp82HempAn147e6BXijWDzz9g7/2nSebVP9GVg62Fz5ZAs55mxq13GA0XLyvY8XkyDjg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/validation": "1.4.3", + "@webassemblyjs/wasm-parser": "1.4.3", + "@webassemblyjs/wast-parser": "1.4.3", + "long": "3.2.0" + } + }, + "webdriver-js-extender": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-1.0.0.tgz", + "integrity": "sha1-gcUzqeM9W/tZe05j4s2yW1R3dRU=", + "dev": true, + "requires": { + "@types/selenium-webdriver": "2.53.43", + "selenium-webdriver": "2.53.3" + }, + "dependencies": { + "sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + }, + "selenium-webdriver": { + "version": "2.53.3", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", + "integrity": "sha1-0p/1qVff8aG0ncRXdW5OS/vc4IU=", + "dev": true, + "requires": { + "adm-zip": "0.4.4", + "rimraf": "2.6.2", + "tmp": "0.0.24", + "ws": "1.1.2", + "xml2js": "0.4.4" + } + }, + "tmp": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz", + "integrity": "sha1-1qXhmNFKmDXMby18PZ4wJCjIzxI=", + "dev": true + }, + "xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "dev": true, + "requires": { + "sax": "0.6.1", + "xmlbuilder": "9.0.7" + } + } + } + }, + "webpack": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.8.3.tgz", + "integrity": "sha512-/hfAjBISycdK597lxONjKEFX7dSIU1PsYwC3XlXUXoykWBlv9QV5HnO+ql3HvrrgfBJ7WXdnjO9iGPR2aAc5sw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.4.3", + "@webassemblyjs/wasm-edit": "1.4.3", + "@webassemblyjs/wasm-parser": "1.4.3", + "acorn": "5.7.1", + "acorn-dynamic-import": "3.0.0", + "ajv": "6.4.0", + "ajv-keywords": "3.2.0", + "chrome-trace-event": "0.1.3", + "enhanced-resolve": "4.0.0", + "eslint-scope": "3.7.1", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "micromatch": "3.1.10", + "mkdirp": "0.5.1", + "neo-async": "2.5.1", + "node-libs-browser": "2.1.0", + "schema-utils": "0.4.5", + "tapable": "1.0.0", + "uglifyjs-webpack-plugin": "1.2.7", + "watchpack": "1.6.0", + "webpack-sources": "1.1.0" + } + }, + "webpack-core": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", + "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", + "dev": true, + "requires": { + "source-list-map": "0.1.8", + "source-map": "0.4.4" + }, + "dependencies": { + "source-list-map": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", + "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.1.3.tgz", + "integrity": "sha512-I6Mmy/QjWU/kXwCSFGaiOoL5YEQIVmbb0o45xMoCyQAg/mClqZVTcsX327sPfekDyJWpCxb+04whNyLOIxpJdQ==", + "dev": true, + "requires": { + "loud-rejection": "1.6.0", + "memory-fs": "0.4.1", + "mime": "2.3.1", + "path-is-absolute": "1.0.1", + "range-parser": "1.2.0", + "url-join": "4.0.0", + "webpack-log": "1.2.0" + }, + "dependencies": { + "mime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "dev": true + } + } + }, + "webpack-dev-server": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz", + "integrity": "sha512-itcIUDFkHuj1/QQxzUFOEXXmxOj5bku2ScLEsOFPapnq2JRTm58gPdtnBphBJOKL2+M3p6+xygL64bI+3eyzzw==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "array-includes": "3.0.3", + "bonjour": "3.5.0", + "chokidar": "2.0.4", + "compression": "1.7.2", + "connect-history-api-fallback": "1.5.0", + "debug": "3.1.0", + "del": "3.0.0", + "express": "4.16.3", + "html-entities": "1.2.1", + "http-proxy-middleware": "0.18.0", + "import-local": "1.0.0", + "internal-ip": "1.2.0", + "ip": "1.1.5", + "killable": "1.0.0", + "loglevel": "1.6.1", + "opn": "5.3.0", + "portfinder": "1.0.13", + "selfsigned": "1.10.3", + "serve-index": "1.9.1", + "sockjs": "0.3.19", + "sockjs-client": "1.1.4", + "spdy": "3.4.7", + "strip-ansi": "3.0.1", + "supports-color": "5.4.0", + "webpack-dev-middleware": "3.1.3", + "webpack-log": "1.2.0", + "yargs": "11.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", + "integrity": "sha512-Rjp+lMYQOWtgqojx1dEWorjCofi1YN7AoFvYV7b1gx/7dAAeuI4kN5SZiEvr0ZmsZTOpDRcCqrpI10L31tFkBw==", + "dev": true, + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "webpack-log": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.2.0.tgz", + "integrity": "sha512-U9AnICnu50HXtiqiDxuli5gLB5PGBo7VvcHx36jRZHwK4vzOYLbImqT4lwWwoMHdQWwEKw736fCHEekokTEKHA==", + "dev": true, + "requires": { + "chalk": "2.2.2", + "log-symbols": "2.2.0", + "loglevelnext": "1.0.5", + "uuid": "3.2.1" + } + }, + "webpack-merge": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.3.tgz", + "integrity": "sha512-zxwAIGK7nKdu5CIZL0BjTQoq3elV0t0MfB7rUC1zj668geid52abs6hN/ACwZdK6LeMS8dC9B6WmtF978zH5mg==", + "dev": true, + "requires": { + "lodash": "4.17.10" + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "dev": true, + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "webpack-subresource-integrity": { + "version": "1.1.0-rc.4", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.4.tgz", + "integrity": "sha1-xcTj1pD50vZKlVDgeodn+Xlqpdg=", + "dev": true, + "requires": { + "webpack-core": "0.6.9" + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "dev": true, + "requires": { + "http-parser-js": "0.4.13", + "websocket-extensions": "0.1.3" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "when": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", + "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "1.0.2" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "dev": true, + "requires": { + "errno": "0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", + "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", + "dev": true, + "requires": { + "options": "0.0.6", + "ultron": "1.0.2" + } + }, + "wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.7" + }, + "dependencies": { + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + } + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "xxhashjs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", + "dev": true, + "requires": { + "cuint": "0.2.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true, + "optional": true + } + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + }, + "zone.js": { + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz", + "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA==" + } + } +} diff --git a/gae/frontend/package.json b/gae/frontend/package.json new file mode 100644 index 0000000..6d722be --- /dev/null +++ b/gae/frontend/package.json @@ -0,0 +1,48 @@ +{ + "name": "frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "^6.0.3", + "@angular/common": "^6.0.3", + "@angular/compiler": "^6.0.3", + "@angular/core": "^6.0.3", + "@angular/forms": "^6.0.3", + "@angular/http": "^6.0.3", + "@angular/platform-browser": "^6.0.3", + "@angular/platform-browser-dynamic": "^6.0.3", + "@angular/router": "^6.0.3", + "core-js": "^2.5.4", + "rxjs": "^6.0.0", + "zone.js": "^0.8.26" + }, + "devDependencies": { + "@angular/compiler-cli": "^6.0.3", + "@angular-devkit/build-angular": "~0.6.8", + "typescript": "~2.7.2", + "@angular/cli": "~6.0.8", + "@angular/language-service": "^6.0.3", + "@types/jasmine": "~2.8.6", + "@types/jasminewd2": "~2.0.3", + "@types/node": "~8.9.4", + "codelyzer": "~4.2.1", + "jasmine-core": "~2.99.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~1.7.1", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage-istanbul-reporter": "~2.0.0", + "karma-jasmine": "~1.1.1", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.3.0", + "ts-node": "~5.0.1", + "tslint": "~5.9.1" + } +} diff --git a/gae/frontend/src/app/app.component.css b/gae/frontend/src/app/app.component.css new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/app.component.html b/gae/frontend/src/app/app.component.html new file mode 100644 index 0000000..fa2706a --- /dev/null +++ b/gae/frontend/src/app/app.component.html @@ -0,0 +1,20 @@ + +
+

+ Welcome to {{ title }}! +

+ Angular Logo +
+

Here are some links to help you start:

+ + diff --git a/gae/frontend/src/app/app.component.spec.ts b/gae/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..decc558 --- /dev/null +++ b/gae/frontend/src/app/app.component.spec.ts @@ -0,0 +1,27 @@ +import { TestBed, async } from '@angular/core/testing'; +import { AppComponent } from './app.component'; +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); + it(`should have as title 'app'`, async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('app'); + })); + it('should render title in a h1 tag', async(() => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('Welcome to frontend!'); + })); +}); diff --git a/gae/frontend/src/app/app.component.ts b/gae/frontend/src/app/app.component.ts new file mode 100644 index 0000000..7b0f672 --- /dev/null +++ b/gae/frontend/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'app'; +} diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts new file mode 100644 index 0000000..f657163 --- /dev/null +++ b/gae/frontend/src/app/app.module.ts @@ -0,0 +1,16 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; + +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/gae/frontend/src/assets/.gitkeep b/gae/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/browserslist b/gae/frontend/src/browserslist new file mode 100644 index 0000000..8e09ab4 --- /dev/null +++ b/gae/frontend/src/browserslist @@ -0,0 +1,9 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# For IE 9-11 support, please uncomment the last line of the file and adjust as needed +> 0.5% +last 2 versions +Firefox ESR +not dead +# IE 9-11 \ No newline at end of file diff --git a/gae/frontend/src/environments/environment.prod.ts b/gae/frontend/src/environments/environment.prod.ts new file mode 100644 index 0000000..3612073 --- /dev/null +++ b/gae/frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/gae/frontend/src/environments/environment.ts b/gae/frontend/src/environments/environment.ts new file mode 100644 index 0000000..012182e --- /dev/null +++ b/gae/frontend/src/environments/environment.ts @@ -0,0 +1,15 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/gae/frontend/src/favicon.ico b/gae/frontend/src/favicon.ico new file mode 100644 index 0000000..8081c7c Binary files /dev/null and b/gae/frontend/src/favicon.ico differ diff --git a/gae/frontend/src/index.html b/gae/frontend/src/index.html new file mode 100644 index 0000000..3faefb6 --- /dev/null +++ b/gae/frontend/src/index.html @@ -0,0 +1,14 @@ + + + + + Frontend + + + + + + + + + diff --git a/gae/frontend/src/karma.conf.js b/gae/frontend/src/karma.conf.js new file mode 100644 index 0000000..b6e0042 --- /dev/null +++ b/gae/frontend/src/karma.conf.js @@ -0,0 +1,31 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; \ No newline at end of file diff --git a/gae/frontend/src/main.ts b/gae/frontend/src/main.ts new file mode 100644 index 0000000..91ec6da --- /dev/null +++ b/gae/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.log(err)); diff --git a/gae/frontend/src/polyfills.ts b/gae/frontend/src/polyfills.ts new file mode 100644 index 0000000..d310405 --- /dev/null +++ b/gae/frontend/src/polyfills.ts @@ -0,0 +1,80 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/gae/frontend/src/styles.css b/gae/frontend/src/styles.css new file mode 100644 index 0000000..90d4ee0 --- /dev/null +++ b/gae/frontend/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/gae/frontend/src/test.ts b/gae/frontend/src/test.ts new file mode 100644 index 0000000..1631789 --- /dev/null +++ b/gae/frontend/src/test.ts @@ -0,0 +1,20 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/gae/frontend/src/tsconfig.app.json b/gae/frontend/src/tsconfig.app.json new file mode 100644 index 0000000..722c370 --- /dev/null +++ b/gae/frontend/src/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "es2015", + "types": [] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/gae/frontend/src/tsconfig.spec.json b/gae/frontend/src/tsconfig.spec.json new file mode 100644 index 0000000..8f7cede --- /dev/null +++ b/gae/frontend/src/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "module": "commonjs", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/gae/frontend/src/tslint.json b/gae/frontend/src/tslint.json new file mode 100644 index 0000000..52e2c1a --- /dev/null +++ b/gae/frontend/src/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ] + } +} diff --git a/gae/frontend/tsconfig.json b/gae/frontend/tsconfig.json new file mode 100644 index 0000000..ef44e28 --- /dev/null +++ b/gae/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git a/gae/frontend/tslint.json b/gae/frontend/tslint.json new file mode 100644 index 0000000..3ea984c --- /dev/null +++ b/gae/frontend/tslint.json @@ -0,0 +1,130 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git a/gae/worker.yaml b/gae/worker.yaml index 1d05414..7d6b859 100644 --- a/gae/worker.yaml +++ b/gae/worker.yaml @@ -7,3 +7,16 @@ handlers: - url: /.* script: webapp.src.worker_main.app login: admin + +# [START exclude] +skip_files: +- ^(.*/)?#.*#$ +- ^(.*/)?.*~$ +- ^(.*/)?.*\.py[co]$ +- ^(.*/)?.*/RCS/.*$ +- ^(.*/)?\..*$ +- ^script/*$ +- .*_test.py$ +- ^(.*/)?frontend/(.*) +- ^(.*/)?\.idea/(.*) +# [END exclude] -- cgit v1.2.3 From b9c01ac72564c46f3e3962bd0ebdefd96505abbb Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 12 Jul 2018 17:43:18 +0900 Subject: Support keyword arguments for unittest functions. Test: python testing/e2e_test.py Bug: 77617865 --- gae/webapp/src/tasks/indexing_test.py | 2 + gae/webapp/src/testing/unittest_base.py | 92 +++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/gae/webapp/src/tasks/indexing_test.py b/gae/webapp/src/tasks/indexing_test.py index 7ef8ea3..5d14a56 100644 --- a/gae/webapp/src/tasks/indexing_test.py +++ b/gae/webapp/src/tasks/indexing_test.py @@ -68,6 +68,7 @@ class IndexingHandlerTest(unittest_base.UnitTestBase): job.gsi_branch = schedule.gsi_branch job.gsi_build_target = schedule.gsi_build_target job.gsi_pab_account_id = schedule.gsi_pab_account_id + job.gsi_vendor_version = schedule.gsi_vendor_version job.test_storage_type = schedule.test_storage_type job.test_branch = schedule.test_branch job.test_build_target = schedule.test_build_target @@ -140,6 +141,7 @@ class IndexingHandlerTest(unittest_base.UnitTestBase): job.gsi_branch = schedule.gsi_branch job.gsi_build_target = schedule.gsi_build_target job.gsi_pab_account_id = schedule.gsi_pab_account_id + job.gsi_vendor_version = schedule.gsi_vendor_version job.test_storage_type = schedule.test_storage_type job.test_branch = schedule.test_branch job.test_build_target = schedule.test_build_target diff --git a/gae/webapp/src/testing/unittest_base.py b/gae/webapp/src/testing/unittest_base.py index 684f692..bfecc40 100644 --- a/gae/webapp/src/testing/unittest_base.py +++ b/gae/webapp/src/testing/unittest_base.py @@ -91,30 +91,41 @@ class UnitTestBase(unittest.TestCase): def GenerateDeviceModel( self, - hostname=None, - product=None, - serial=None, status=Status.DEVICE_STATUS_DICT["fastboot"], - scheduling_status=Status.DEVICE_SCHEDULING_STATUS_DICT["free"]): + scheduling_status=Status.DEVICE_SCHEDULING_STATUS_DICT["free"], + **kwargs): """Builds model.DeviceModel with given information. Args: - hostname: a string, host name. - product: a string, device product name. - serial: a string, device serial number. status: an integer, device's initial status. scheduling_status: an integer, device's initial scheduling status. + **kwargs: the optional arguments. Returns: model.DeviceModel instance. """ device = model.DeviceModel() - device.hostname = hostname if hostname else self.GetRandomString() - device.product = product if product else self.GetRandomString() - device.serial = serial if serial else self.GetRandomString() device.status = status device.scheduling_status = scheduling_status device.timestamp = datetime.datetime.now() + + for arg in device._properties: + if arg in ["status", "scheduling_status", "timestamp"]: + continue + if arg in kwargs: + value = kwargs[arg] + elif isinstance(device._properties[arg], ndb.StringProperty): + value = self.GetRandomString() + elif isinstance(device._properties[arg], ndb.IntegerProperty): + value = 0 + elif isinstance(device._properties[arg], ndb.BooleanProperty): + value = False + else: + print("A type of property '{}' is not supported.".format(arg)) + continue + if device._properties[arg]._repeated: + value = [value] + setattr(device, arg, value) return device def GenerateScheduleModel( @@ -122,49 +133,39 @@ class UnitTestBase(unittest.TestCase): device_model=None, lab_model=None, priority="medium", - test_name=None, period=360, retry_count=1, shards=1, lab_name=None, device_storage_type=Status.STORAGE_TYPE_DICT["PAB"], - device_pab_account_id=None, device_branch=None, device_target=None, gsi_storage_type=Status.STORAGE_TYPE_DICT["PAB"], - gsi_pab_account_id=None, - gsi_branch=None, gsi_build_target=None, test_storage_type=Status.STORAGE_TYPE_DICT["PAB"], - test_pab_account_id=None, - test_branch=None, test_build_target=None, - required_signed_device_build=False): + required_signed_device_build=False, + **kwargs): """Builds model.ScheduleModel with given information. Args: device_model: a model.DeviceModel instance to refer device product. lab_model: a model.LabModel instance to refer host name. priority: a string, scheduling priority - test_name: a string, schedule test name. period: an integer, scheduling period. retry_count: an integer, scheduling retry count. shards: an integer, # ways of device shards. lab_name: a string, target lab name. device_storage_type: an integer, device storage type - device_pab_account_id: a string, device PAB account ID. device_branch: a string, device build branch. device_target: a string, device build target. gsi_storage_type: an integer, GSI storage type - gsi_pab_account_id: a string, GSI PAB account ID. - gsi_branch: a string, GSI build branch. gsi_build_target: a string, GSI build target. test_storage_type: an integer, test storage type - test_pab_account_id: a string, test PAB account ID. - test_branch: a string, test build branch. test_build_target: a string, test build target. required_signed_device_build: a boolean, True to schedule for signed device build, False if not. + **kwargs: the optional arguments. Returns: model.ScheduleModel instance. @@ -188,42 +189,57 @@ class UnitTestBase(unittest.TestCase): schedule = model.ScheduleModel() schedule.priority = priority - schedule.test_name = test_name if test_name else self.GetRandomString() + schedule.priority_value = Status.GetPriorityValue(schedule.priority) schedule.period = period schedule.shards = shards schedule.retry_count = retry_count - schedule.build_storage_type = device_storage_type schedule.required_signed_device_build = required_signed_device_build + schedule.build_storage_type = device_storage_type schedule.manifest_branch = (device_branch if device_branch else self.GetRandomString()) schedule.build_target = "-".join([device_product, device_target]) - schedule.device_pab_account_id = (device_pab_account_id - if device_pab_account_id else - self.GetRandomString()) + schedule.gsi_storage_type = gsi_storage_type - schedule.gsi_branch = (gsi_branch - if gsi_branch else self.GetRandomString()) schedule.gsi_build_target = (gsi_build_target if gsi_build_target else "-".join([ self.GetRandomString(), self.GetRandomString(4) ])) - schedule.gsi_pab_account_id = (gsi_pab_account_id if gsi_pab_account_id - else self.GetRandomString()) schedule.test_storage_type = test_storage_type - schedule.test_branch = (test_branch - if test_branch else self.GetRandomString()) schedule.test_build_target = (test_build_target if test_build_target else "-".join([ self.GetRandomString(), self.GetRandomString(4) ])) - schedule.test_pab_account_id = (test_pab_account_id - if test_pab_account_id else - self.GetRandomString()) schedule.device = [] schedule.device.append("/".join([lab, device_product])) - schedule.priority_value = Status.GetPriorityValue(schedule.priority) + + for arg in schedule._properties: + if arg in [ + "priority", "priority_value", "period", "shards", + "retry_count", "required_signed_device_build", + "build_storage_type", "manifest_branch", "build_target", + "gsi_storage_type", "gsi_build_target", + "test_storage_type", "test_build_target", "device", + "children_jobs", "timestamp", "required_host_equipment", + "required_device_equipment" + ]: + continue + if arg in kwargs: + value = kwargs[arg] + elif isinstance(schedule._properties[arg], ndb.StringProperty): + value = self.GetRandomString() + elif isinstance(schedule._properties[arg], ndb.IntegerProperty): + value = 0 + elif isinstance(schedule._properties[arg], ndb.BooleanProperty): + value = False + else: + print("A type of property '{}' is not supported.".format(arg)) + continue + if schedule._properties[arg]._repeated: + value = [value] + setattr(schedule, arg, value) + return schedule def GenerateBuildModel(self, schedule, targets=None): -- cgit v1.2.3 From 4f356ecf4ecc0395e10525596b2ff90956a4acdc Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 16 Jul 2018 17:01:05 +0900 Subject: Create a schedule with minimum equipment. Test: python testing/e2e_test.py Bug: 111486439 Change-Id: I6f97b2195d972a9bf6f5db33927c2305e5b02a5a --- gae/webapp/src/scheduler/schedule_worker.py | 88 +++++++++------- gae/webapp/src/scheduler/schedule_worker_test.py | 129 +++++++++++++++++++++++ gae/webapp/src/testing/unittest_base.py | 26 ++--- 3 files changed, 193 insertions(+), 50 deletions(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index fcb0f13..08fc9f7 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -421,59 +421,71 @@ class ScheduleHandler(webapp2.RequestHandler): a list of selected devices serial (see whether devices will be selected later when the job is picked up.) """ + + available_devices = [] for target_device in schedule.device: if "/" not in target_device: - # device malformed + self.logger.Println( + "Device malformed - {}".format(target_device)) continue target_lab, target_product_type = target_device.split("/") self.logger.Println("- Lab %s" % target_lab) self.logger.Indent() - lab_query = model.LabModel.query(model.LabModel.name == target_lab) - target_labs = lab_query.fetch() + host_query = model.LabModel.query( + model.LabModel.name == target_lab) + target_hosts = host_query.fetch() - available_devices = {} - if target_labs: - for lab in target_labs: + if target_hosts: + for host in target_hosts: if not (set(schedule.required_host_equipment) <= set( - lab.host_equipment)): + host.host_equipment)): continue - self.logger.Println("- Host: %s" % lab.hostname) + self.logger.Println("- Host: %s" % host.hostname) self.logger.Indent() device_query = model.DeviceModel.query( - model.DeviceModel.hostname == lab.hostname) + model.DeviceModel.hostname == host.hostname, + model.DeviceModel.scheduling_status == + Status.DEVICE_SCHEDULING_STATUS_DICT["free"], + model.DeviceModel.status.IN([ + Status.DEVICE_STATUS_DICT["fastboot"], + Status.DEVICE_STATUS_DICT["online"], + Status.DEVICE_STATUS_DICT["ready"] + ])) host_devices = device_query.fetch() - - for device in host_devices: - if ((device.status in [ - Status.DEVICE_STATUS_DICT["fastboot"], - Status.DEVICE_STATUS_DICT["online"], - Status.DEVICE_STATUS_DICT["ready"] - ]) and (device.scheduling_status == - Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) - and device.product.lower() == - target_product_type.lower() - and (set(schedule.required_device_equipment) <= - set(device.device_equipment))): - self.logger.Println("- Found %s %s %s" % - (device.product, device.status, - device.serial)) - if device.hostname not in available_devices: - available_devices[device.hostname] = set() - available_devices[device.hostname].add( - device.serial) - self.logger.Unindent() - for host in available_devices: - if len(available_devices[host]) >= schedule.shards: - self.logger.Println("All devices found.") + host_devices = [ + x for x in host_devices + if x.product.lower() == target_product_type.lower() and + (set(schedule.required_device_equipment) <= set( + x.device_equipment)) + ] + if len(host_devices) < schedule.shards: + self.logger.Println( + "A host {} does not have enough devices. " + "# of devices = {}, shards = {}".format( + host.hostname, len(host_devices), + schedule.shards)) self.logger.Unindent() - return host, target_device, list( - available_devices[host])[:schedule.shards] - self.logger.Println( - "- Not enough devices found, while %s required.\n%s" % - (schedule.shards, available_devices)) + continue + host_devices.sort( + key=lambda x: (len(x.device_equipment) + if x.device_equipment else 0)) + available_devices.append(host_devices) + self.logger.Unindent() + self.logger.Unindent() - return None, None, [] + + if not available_devices: + self.logger.Println("No hosts have enough devices for schedule!") + return None, None, [] + + available_devices.sort(lambda x, y: ( + sum([len(y.device_equipment) for y in x[:schedule.shards]]))) + selected_host_devices = available_devices[0] + return selected_host_devices[0].hostname, selected_host_devices[ + 0].product, [ + x.serial for x in selected_host_devices[:schedule.shards] + ] def GetProductName(self, schedule): """Gets a product name from schedule instance. diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index d31fc8e..dfb592f 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -370,6 +370,135 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): jobs = model.JobModel.query().fetch() self.assertEqual(3, len(jobs)) + def testSimpleDevicePriorityWithEquipment(self): + """Asserts a scheduler creates a job with minimum device equipment.""" + equipment_a = "equipment_a" + equipment_b = "equipment_b" + + device_product = "device_product" + lab = self.GenerateLabModel() + lab.put() + + device_a = self.GenerateDeviceModel( + product=device_product, + hostname=lab.hostname, + device_equipment=[equipment_a]) + device_a.put() + + device_b = self.GenerateDeviceModel( + product=device_product, + hostname=lab.hostname, + device_equipment=[equipment_b]) + device_b.put() + + device_c = self.GenerateDeviceModel( + product=device_product, hostname=lab.hostname) + device_c.put() + + schedule = self.GenerateScheduleModel( + device_target="{}-test".format(device_product), + lab_model=lab, + required_device_equipment=[equipment_b]) + schedule.put() + + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() + + # a job should be created and it should be created with equipment_b + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(1, len(jobs)) + self.assertIn(device_b.serial, jobs[0].serial) + + def testDevicePriorityWithEquipment(self): + """Asserts a scheduler creates a job with minimum device equipment.""" + lab_1 = "lab_1" + lab_2 = "lab_2" + + host_a = "host_a" + host_b = "host_b" + host_c = "host_c" + host_d = "host_d" + host_e = "host_e" + + equipment_a = "equipment_a" + equipment_b = "equipment_b" + equipment_c = "equipment_c" + + correct_product = "correct" + wrong_product = "wrong" + + self.GenerateLabModel(lab_name=lab_1, host_name=host_a).put() + self.GenerateLabModel(lab_name=lab_1, host_name=host_b).put() + self.GenerateLabModel(lab_name=lab_2, host_name=host_c).put() + self.GenerateLabModel(lab_name=lab_2, host_name=host_d).put() + self.GenerateLabModel(lab_name=lab_2, host_name=host_e).put() + + # setting devices through host a to e. + equipments = [[equipment_a], [equipment_a], [equipment_b], + [equipment_a, equipment_b]] + for equipment in equipments: + device = self.GenerateDeviceModel( + product=correct_product, hostname=host_a) + device.device_equipment = equipment + device.put() + + equipments = [[], [equipment_a], [equipment_a, equipment_b], + [equipment_a, equipment_b]] + for equipment in equipments: + device = self.GenerateDeviceModel( + product=correct_product, hostname=host_b) + device.device_equipment = equipment + device.put() + + equipments = [[equipment_a], [equipment_a], [equipment_b], + [equipment_b]] + for equipment in equipments: + device = self.GenerateDeviceModel( + product=correct_product, hostname=host_c) + device.device_equipment = equipment + device.put() + + equipments = [[equipment_a], [equipment_a, equipment_b, equipment_c], + [equipment_a, equipment_b]] + for equipment in equipments: + device = self.GenerateDeviceModel( + product=correct_product, hostname=host_d) + device.device_equipment = equipment + device.put() + + products = [correct_product, correct_product, wrong_product] + for product in products: + device = self.GenerateDeviceModel(product=product, hostname=host_e) + device.device_equipment = [equipment_a] + device.put() + + schedule = self.GenerateScheduleModel( + device_target="{}-test".format(correct_product), shards=3) + schedule.required_device_equipment = [equipment_a] + schedule.device = [ + "{}/{}".format(lab_1, correct_product), "{}/{}".format( + lab_2, correct_product) + ] + schedule.put() + + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() + + # a job should be created on host_a + self.scheduler.post() + jobs = model.JobModel.query().fetch() + self.assertEqual(1, len(jobs)) + + host_a_devices = model.DeviceModel.query( + model.DeviceModel.hostname == host_a).fetch() + host_a_devices_serial = [x.serial for x in host_a_devices] + + for job_device in jobs[0].serial: + self.assertIn(job_device, host_a_devices_serial) + if __name__ == "__main__": unittest.main() diff --git a/gae/webapp/src/testing/unittest_base.py b/gae/webapp/src/testing/unittest_base.py index bfecc40..04981b4 100644 --- a/gae/webapp/src/testing/unittest_base.py +++ b/gae/webapp/src/testing/unittest_base.py @@ -109,8 +109,10 @@ class UnitTestBase(unittest.TestCase): device.scheduling_status = scheduling_status device.timestamp = datetime.datetime.now() + skip_list = ["status", "scheduling_status", "timestamp"] + set_or_empty = [] for arg in device._properties: - if arg in ["status", "scheduling_status", "timestamp"]: + if arg in skip_list or (arg in set_or_empty and arg not in kwargs): continue if arg in kwargs: value = kwargs[arg] @@ -123,7 +125,7 @@ class UnitTestBase(unittest.TestCase): else: print("A type of property '{}' is not supported.".format(arg)) continue - if device._properties[arg]._repeated: + if device._properties[arg]._repeated and type(value) is not list: value = [value] setattr(device, arg, value) return device @@ -214,16 +216,16 @@ class UnitTestBase(unittest.TestCase): schedule.device = [] schedule.device.append("/".join([lab, device_product])) + skip_list = [ + "priority", "priority_value", "period", "shards", + "retry_count", "required_signed_device_build", + "build_storage_type", "manifest_branch", "build_target", + "gsi_storage_type", "gsi_build_target", + "test_storage_type", "test_build_target", "device", + "children_jobs", "timestamp"] + set_or_empty = ["required_host_equipment", "required_device_equipment"] for arg in schedule._properties: - if arg in [ - "priority", "priority_value", "period", "shards", - "retry_count", "required_signed_device_build", - "build_storage_type", "manifest_branch", "build_target", - "gsi_storage_type", "gsi_build_target", - "test_storage_type", "test_build_target", "device", - "children_jobs", "timestamp", "required_host_equipment", - "required_device_equipment" - ]: + if arg in skip_list or (arg in set_or_empty and arg not in kwargs): continue if arg in kwargs: value = kwargs[arg] @@ -236,7 +238,7 @@ class UnitTestBase(unittest.TestCase): else: print("A type of property '{}' is not supported.".format(arg)) continue - if schedule._properties[arg]._repeated: + if schedule._properties[arg]._repeated and type(value) is not list: value = [value] setattr(schedule, arg, value) -- cgit v1.2.3 From 8a0e1d9298afc50a0ce3a8e05dd06e8a8ca31fe0 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 17 Jul 2018 14:12:34 +0900 Subject: Correct lambda function in sort method. Test: python testing/e2e_test.py Bug: 111486439 --- gae/webapp/src/scheduler/schedule_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 08fc9f7..4b98b37 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -479,7 +479,7 @@ class ScheduleHandler(webapp2.RequestHandler): self.logger.Println("No hosts have enough devices for schedule!") return None, None, [] - available_devices.sort(lambda x, y: ( + available_devices.sort(key=lambda x: ( sum([len(y.device_equipment) for y in x[:schedule.shards]]))) selected_host_devices = available_devices[0] return selected_host_devices[0].hostname, selected_host_devices[ -- cgit v1.2.3 From eb9cb8be2ccc21363c8c4a185732ee4565ebc73b Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 17 Jul 2018 18:15:34 +0900 Subject: Fix endpoint testcases. Test: python testing/e2e_test.py Bug: 79717764 --- gae/webapp/src/endpoint/schedule_info_test.py | 114 +++++++++++++------------- gae/webapp/src/proto/model.py | 4 +- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/gae/webapp/src/endpoint/schedule_info_test.py b/gae/webapp/src/endpoint/schedule_info_test.py index d946733..82df829 100644 --- a/gae/webapp/src/endpoint/schedule_info_test.py +++ b/gae/webapp/src/endpoint/schedule_info_test.py @@ -42,36 +42,35 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): def testSetWithSimpleMessage(self): """Asserts schedule_info/set API receives a simple message.""" # As of June 8, 2018, these are uploaded from host controller. - message = model.ScheduleInfoMessage() - message.manifest_branch = self.GetRandomString() - message.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - message.build_target = self.GetRandomString() - message.require_signed_device_build = False - message.has_bootloader_img = True - message.has_radio_img = True - message.test_name = self.GetRandomString() - message.period = 360 - message.priority = "high" - message.device = [self.GetRandomString()] - message.required_host_equipment = [self.GetRandomString()] - message.required_device_equipment = [self.GetRandomString()] - message.device_pab_account_id = self.GetRandomString() - message.shards = 1 - message.param = [self.GetRandomString()] - message.retry_count = 1 - message.gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - message.gsi_branch = self.GetRandomString() - message.gsi_build_target = self.GetRandomString() - message.gsi_pab_account_id = self.GetRandomString() - message.gsi_vendor_version = self.GetRandomString() - message.test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - message.test_branch = self.GetRandomString() - message.test_build_target = self.GetRandomString() - message.test_pab_account_id = self.GetRandomString() - # message.image_package_repo_base = self.GetRandomString() - container = ( - schedule_info.SCHEDULE_INFO_RESOURCE.combined_message_class()) + schedule_info.SCHEDULE_INFO_RESOURCE.combined_message_class( + manifest_branch=self.GetRandomString(), + build_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + build_target=self.GetRandomString(), + require_signed_device_build=False, + has_bootloader_img=True, + has_radio_img=True, + test_name=self.GetRandomString(), + period=360, + priority="high", + device=[self.GetRandomString()], + required_host_equipment=[self.GetRandomString()], + required_device_equipment=[self.GetRandomString()], + device_pab_account_id=self.GetRandomString(), + shards=1, + param=[self.GetRandomString()], + retry_count=1, + gsi_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + gsi_branch=self.GetRandomString(), + gsi_build_target=self.GetRandomString(), + gsi_pab_account_id=self.GetRandomString(), + gsi_vendor_version=self.GetRandomString(), + test_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + test_branch=self.GetRandomString(), + test_build_target=self.GetRandomString(), + test_pab_account_id=self.GetRandomString(), + image_package_repo_base=self.GetRandomString(), + )) api = schedule_info.ScheduleInfoApi() response = api.set(container) @@ -84,36 +83,35 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): method. """ # As of June 8, 2018, these are uploaded from host controller. - message = model.ScheduleInfoMessage() - message.manifest_branch = self.GetRandomString() - message.build_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - message.build_target = self.GetRandomString() - message.require_signed_device_build = False - message.has_bootloader_img = True - message.has_radio_img = True - message.test_name = self.GetRandomString() - message.period = 360 - message.priority = "high" - message.device = [self.GetRandomString()] - message.required_host_equipment = [] - message.required_device_equipment = [self.GetRandomString()] - message.device_pab_account_id = self.GetRandomString() - message.shards = 1 - message.param = [self.GetRandomString()] - message.retry_count = 1 - message.gsi_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - message.gsi_branch = self.GetRandomString() - message.gsi_build_target = self.GetRandomString() - message.gsi_pab_account_id = self.GetRandomString() - message.gsi_vendor_version = self.GetRandomString() - message.test_storage_type = Status.STORAGE_TYPE_DICT["PAB"] - message.test_branch = self.GetRandomString() - message.test_build_target = self.GetRandomString() - message.test_pab_account_id = self.GetRandomString() - # message.image_package_repo_base = self.GetRandomString() - container = ( - schedule_info.SCHEDULE_INFO_RESOURCE.combined_message_class()) + schedule_info.SCHEDULE_INFO_RESOURCE.combined_message_class( + manifest_branch=self.GetRandomString(), + build_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + build_target=self.GetRandomString(), + require_signed_device_build=False, + has_bootloader_img=True, + has_radio_img=True, + test_name=self.GetRandomString(), + period=360, + priority="high", + device=[self.GetRandomString()], + required_host_equipment=[self.GetRandomString()], + required_device_equipment=[self.GetRandomString()], + device_pab_account_id=self.GetRandomString(), + shards=1, + param=[self.GetRandomString()], + retry_count=1, + gsi_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + gsi_branch=self.GetRandomString(), + gsi_build_target=self.GetRandomString(), + gsi_pab_account_id=self.GetRandomString(), + gsi_vendor_version=self.GetRandomString(), + test_storage_type=Status.STORAGE_TYPE_DICT["PAB"], + test_branch=self.GetRandomString(), + test_build_target=self.GetRandomString(), + test_pab_account_id=self.GetRandomString(), + image_package_repo_base=self.GetRandomString(), + )) api = schedule_info.ScheduleInfoApi() response = api.set(container) diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 8912f7b..7a300f7 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -109,7 +109,7 @@ class ScheduleControlInfoMessage(messages.Message): class ScheduleInfoMessage(messages.Message): """A message for representing an individual schedule entry.""" - # Next ID = 31 + # Next ID = 32 # schedule name for green build schedule, optional. name = messages.StringField(16) schedule_type = messages.StringField(19) @@ -150,6 +150,8 @@ class ScheduleInfoMessage(messages.Message): report_bucket = messages.StringField(29, repeated=True) report_spreadsheet_id = messages.StringField(30, repeated=True) + image_package_repo_base = messages.StringField(31) + class LabModel(ndb.Model): """A model for representing an individual lab entry.""" -- cgit v1.2.3 From 28679c72528ff91c3c046ae4f97b1a00e7ae413c Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 17 Jul 2018 18:06:57 +0900 Subject: Create common endpoint base class. Test: python testing/e2e_test.py Bug: 111531361 Change-Id: I68fe4cc94f2ed7f3acf8bba5d00d91d026dbe311 --- gae/webapp/src/endpoint/build_info.py | 5 +- gae/webapp/src/endpoint/endpoint_base.py | 90 +++++++++++++++++++++++ gae/webapp/src/endpoint/endpoint_base_test.py | 100 ++++++++++++++++++++++++++ gae/webapp/src/endpoint/host_info.py | 5 +- gae/webapp/src/endpoint/job_queue.py | 5 +- gae/webapp/src/endpoint/lab_info.py | 5 +- gae/webapp/src/endpoint/schedule_info.py | 40 +++++------ 7 files changed, 214 insertions(+), 36 deletions(-) create mode 100644 gae/webapp/src/endpoint/endpoint_base.py create mode 100644 gae/webapp/src/endpoint/endpoint_base_test.py diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index 249e362..d9d0bb4 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -17,15 +17,14 @@ import datetime import endpoints import logging -from protorpc import remote - +from webapp.src.endpoint import endpoint_base from webapp.src.proto import model BUILD_INFO_RESOURCE = endpoints.ResourceContainer(model.BuildInfoMessage) @endpoints.api(name="build_info", version="v1") -class BuildInfoApi(remote.Service): +class BuildInfoApi(endpoint_base.EndpointBase): """Endpoint API for build_info.""" @endpoints.method( diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py new file mode 100644 index 0000000..47a7bef --- /dev/null +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -0,0 +1,90 @@ +# Copyright 2018 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import inspect + +from protorpc import messages +from protorpc import remote +from google.appengine.ext import ndb + + +class EndpointBase(remote.Service): + """A base class for endpoint implementation.""" + + def GetCommonAttributes(self, resource, reference): + """Gets a list of common attribute names. + + This method finds the attributes assigned in 'resource' instance, and + filters out if the attributes are not a member of 'reference' class. + + Args: + resource: either a protorpc.messages.Message instance, + or a ndb.Model instance. + reference: either a protorpc.messages.Message class, + or a ndb.Model class. + + Returns: + a list of string, attribute names exist on resource and reference. + + Raises: + ValueError if resource or reference is not supported class. + """ + # check resource type and absorb list of assigned attributes. + resource_attrs = self.GetAttributes(resource, assigned_only=True) + reference_attrs = self.GetAttributes(reference) + return [x for x in resource_attrs if x in reference_attrs] + + def GetAttributes(self, value, assigned_only=False): + """Gets a list of attributes. + + Args: + value: a class instance or a class itself. + assigned_only: True to get only assigned attributes when value is + an instance, False to get all attributes. + + Raises: + ValueError if value is not supported class. + """ + attrs = [] + if inspect.isclass(value): + if assigned_only: + logging.warning( + "Please use a class instance for 'resource' argument.") + + if (issubclass(value, messages.Message) + or issubclass(value, ndb.Model)): + attrs = [ + x[0] for x in value.__dict__.items() + if not x[0].startswith("_") + ] + else: + raise ValueError("Only protorpc.messages.Message or ndb.Model " + "class are supported.") + else: + if isinstance(value, messages.Message): + attrs = [ + x.name for x in value.all_fields() + if not assigned_only or value.get_assigned_value(x.name) + ] + elif isinstance(value, ndb.Model): + attrs = [ + x for x in list(value.to_dict()) + if not assigned_only or getattr(value, x, None) + ] + else: + raise ValueError("Only protorpc.messages.Message or ndb.Model " + "class are supported.") + + return attrs diff --git a/gae/webapp/src/endpoint/endpoint_base_test.py b/gae/webapp/src/endpoint/endpoint_base_test.py new file mode 100644 index 0000000..a2c9fb7 --- /dev/null +++ b/gae/webapp/src/endpoint/endpoint_base_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src.endpoint import endpoint_base +from webapp.src.proto import model +from webapp.src.testing import unittest_base + + +class EndpointBaseTest(unittest_base.UnitTestBase): + """A class to test endpoint_base.EndpointBase class. """ + + def setUp(self): + """Initializes test""" + super(EndpointBaseTest, self).setUp() + + def testGetAssignedMessagesAttributes(self): + attrs = ["hostname", "priority", "test_branch"] + job_message = model.JobMessage() + for attr in attrs: + setattr(job_message, attr, attr) + eb = endpoint_base.EndpointBase() + result = eb.GetAttributes(job_message, assigned_only=True) + self.assertEquals(set(attrs), set(result)) + + def testGetAssignedModelAttributes(self): + attrs = ["hostname", "priority", "test_branch"] + job = model.JobModel() + for attr in attrs: + setattr(job, attr, attr) + eb = endpoint_base.EndpointBase() + result = eb.GetAttributes(job, assigned_only=True) + self.assertEquals(set(attrs), set(result)) + + def testGetAllMessagesAttributes(self): + attrs = ["hostname", "priority", "test_branch"] + full_attrs = [ + "test_type", "hostname", "priority", "test_name", + "require_signed_device_build", "has_bootloader_img", + "has_radio_img", "device", "serial", "build_storage_type", + "manifest_branch", "build_target", "build_id", "pab_account_id", + "shards", "param", "status", "period", "gsi_storage_type", + "gsi_branch", "gsi_build_target", "gsi_build_id", + "gsi_pab_account_id", "gsi_vendor_version", "test_storage_type", + "test_branch", "test_build_target", "test_build_id", + "test_pab_account_id", "retry_count", "infra_log_url", + "image_package_repo_base", "report_bucket", "report_spreadsheet_id" + ] + job_message = model.JobMessage() + for attr in attrs: + setattr(job_message, attr, attr) + eb = endpoint_base.EndpointBase() + result = eb.GetAttributes(job_message, assigned_only=False) + self.assertTrue(set(full_attrs) <= set(result)) + + def testGetAllModelAttributes(self): + attrs = ["hostname", "priority", "test_branch"] + full_attrs = [ + "test_type", "hostname", "priority", "test_name", + "require_signed_device_build", "has_bootloader_img", + "has_radio_img", "device", "serial", "build_storage_type", + "manifest_branch", "build_target", "build_id", "pab_account_id", + "shards", "param", "status", "period", "gsi_storage_type", + "gsi_branch", "gsi_build_target", "gsi_build_id", + "gsi_pab_account_id", "gsi_vendor_version", "test_storage_type", + "test_branch", "test_build_target", "test_build_id", + "test_pab_account_id", "timestamp", "heartbeat_stamp", + "retry_count", "infra_log_url", "parent_schedule", + "image_package_repo_base", "report_bucket", "report_spreadsheet_id" + ] + job = model.JobModel() + for attr in attrs: + setattr(job, attr, attr) + eb = endpoint_base.EndpointBase() + result = eb.GetAttributes(job, assigned_only=False) + self.assertTrue(set(full_attrs) <= set(result)) + + +if __name__ == "__main__": + unittest.main() diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index 49e84d3..027f163 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -16,11 +16,10 @@ import datetime import endpoints -from protorpc import remote - from google.appengine.api import users from webapp.src import vtslab_status as Status +from webapp.src.endpoint import endpoint_base from webapp.src.proto import model HOST_INFO_RESOURCE = endpoints.ResourceContainer(model.HostInfoMessage) @@ -57,7 +56,7 @@ def AddNullDevices(hostname, null_device_count): @endpoints.api(name='host_info', version='v1') -class HostInfoApi(remote.Service): +class HostInfoApi(endpoint_base.EndpointBase): """Endpoint API for host_info.""" @endpoints.method( diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 652d98e..39a271c 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -18,9 +18,8 @@ import endpoints import logging import re -from protorpc import remote - from webapp.src import vtslab_status as Status +from webapp.src.endpoint import endpoint_base from webapp.src.proto import model from webapp.src.utils import email_util from webapp.src.utils import model_util @@ -32,7 +31,7 @@ STORAGE_API_URL = "https://storage.cloud.google.com/" @endpoints.api(name='job_queue', version='v1') -class JobQueueApi(remote.Service): +class JobQueueApi(endpoint_base.EndpointBase): """Endpoint API for job_queue.""" @endpoints.method( diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 5159a45..215716d 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -18,11 +18,10 @@ import datetime import endpoints import logging -from protorpc import remote - from google.appengine.ext import ndb from webapp.src import vtslab_status as Status +from webapp.src.endpoint import endpoint_base from webapp.src.endpoint import host_info from webapp.src.proto import model @@ -31,7 +30,7 @@ LAB_HOST_INFO_RESOURCE = endpoints.ResourceContainer(model.LabHostInfoMessage) @endpoints.api(name='lab_info', version='v1') -class LabInfoApi(remote.Service): +class LabInfoApi(endpoint_base.EndpointBase): """Endpoint API for lab_info.""" @endpoints.method( diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index a0cbc0e..37805bd 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -16,18 +16,17 @@ import datetime import endpoints -from protorpc import remote - from google.appengine.ext import ndb from webapp.src import vtslab_status as Status +from webapp.src.endpoint import endpoint_base from webapp.src.proto import model SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer(model.ScheduleInfoMessage) @endpoints.api(name="schedule_info", version="v1") -class ScheduleInfoApi(remote.Service): +class ScheduleInfoApi(endpoint_base.EndpointBase): """Endpoint API for schedule_info.""" @endpoints.method( @@ -54,14 +53,7 @@ class ScheduleInfoApi(remote.Service): name="set") def set(self, request): """Sets the schedule info based on `request`.""" - request_fields = request.all_fields() - model_attrs = model.ScheduleModel.__dict__.items() - model_attr_names = [ - x[0] for x in model_attrs if not x[0].startswith("_") - ] - exist_on_both = [ - x for x in request_fields if x.name in model_attr_names - ] + exist_on_both = self.GetCommonAttributes(request, model.ScheduleModel) # check duplicates exclusions = [ "name", "schedule_type", "schedule", "param", "timestamp", @@ -69,24 +61,24 @@ class ScheduleInfoApi(remote.Service): ] # list of protorpc message fields. duplicate_checklist = [ - x for x in exist_on_both if x.name not in exclusions + x for x in exist_on_both if x not in exclusions ] empty_list_field = [] query = model.ScheduleModel.query() - for field in duplicate_checklist: - if field.repeated: - value = request.get_assigned_value(field.name) + for attr_name in duplicate_checklist: + if model.ScheduleModel._properties[attr_name]._repeated: + value = request.get_assigned_value(attr_name) if value: query = query.filter( - getattr(model.ScheduleModel, field.name).IN( - request.get_assigned_value(field.name))) + getattr(model.ScheduleModel, attr_name).IN( + request.get_assigned_value(attr_name))) else: # empty list cannot be queried. - empty_list_field.append(field.name) + empty_list_field.append(attr_name) else: query = query.filter( - getattr(model.ScheduleModel, field.name) == - request.get_assigned_value(field.name)) + getattr(model.ScheduleModel, attr_name) == + request.get_assigned_value(attr_name)) duplicated_schedules = query.fetch() if empty_list_field: @@ -98,9 +90,9 @@ class ScheduleInfoApi(remote.Service): if not duplicated_schedules: schedule = model.ScheduleModel() - for field in exist_on_both: - setattr(schedule, field.name, - request.get_assigned_value(field.name)) + for attr_name in exist_on_both: + setattr(schedule, attr_name, + request.get_assigned_value(attr_name)) schedule.timestamp = datetime.datetime.now() schedule.schedule_type = "test" schedule.error_count = 0 @@ -113,7 +105,7 @@ class ScheduleInfoApi(remote.Service): @endpoints.api(name="green_schedule_info", version="v1") -class GreenScheduleInfoApi(remote.Service): +class GreenScheduleInfoApi(endpoint_base.EndpointBase): """Endpoint API for green_schedule_info.""" @endpoints.method( -- cgit v1.2.3 From 1e3143496428e969239b911aa981b9f40e1da4db Mon Sep 17 00:00:00 2001 From: Hyunwoo Ko Date: Thu, 19 Jul 2018 18:39:38 +0900 Subject: Add report_bucket, report_spreadsheet_id to the job lease msg. Test: mma Bug: 79905934 --- gae/webapp/src/endpoint/job_queue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 652d98e..1ce71c9 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -105,6 +105,8 @@ class JobQueueApi(remote.Service): job_message.image_package_repo_base = job.image_package_repo_base job_message.has_bootloader_img = job.has_bootloader_img job_message.has_radio_img = job.has_radio_img + job_message.report_bucket = job.report_bucket + job_message.report_spreadsheet_id = job.report_spreadsheet_id device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) -- cgit v1.2.3 From b33ac3d20662bc9a2d48bf13775e9fc986ce2855 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 23 Jul 2018 16:29:48 +0900 Subject: Apply common endpoint base to endpoint APIs. This change includes a small bug fix of GetAttributes() method in endpoint_base.EndpointBase that filtering unset properties did not work properly for repeated properties. Test: python testing/e2e_test.py Bug: 111531361 Change-Id: I8b0bd2b7b552a7c68f1d25f2c1a746c8f5a9d8b2 --- gae/webapp/src/endpoint/build_info.py | 11 +- gae/webapp/src/endpoint/build_info_test.py | 139 +++++++++++++++++++++++ gae/webapp/src/endpoint/endpoint_base.py | 6 +- gae/webapp/src/endpoint/job_queue.py | 85 ++------------ gae/webapp/src/endpoint/job_queue_test.py | 154 ++++++++++++++++++++++++++ gae/webapp/src/endpoint/schedule_info_test.py | 10 +- 6 files changed, 316 insertions(+), 89 deletions(-) create mode 100644 gae/webapp/src/endpoint/build_info_test.py create mode 100644 gae/webapp/src/endpoint/job_queue_test.py diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index d9d0bb4..2ac6a63 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -59,14 +59,11 @@ class BuildInfoApi(endpoint_base.EndpointBase): build = None if build: - build.manifest_branch = request.manifest_branch - build.build_id = request.build_id - build.build_target = request.build_target - build.build_type = request.build_type - build.artifact_type = request.artifact_type - build.artifacts = request.artifacts + common_attributes = self.GetCommonAttributes(request, + model.BuildModel) + for attr in common_attributes: + setattr(build, attr, getattr(request, attr)) build.timestamp = datetime.datetime.now() - build.signed = request.signed build.put() return model.DefaultResponse( diff --git a/gae/webapp/src/endpoint/build_info_test.py b/gae/webapp/src/endpoint/build_info_test.py new file mode 100644 index 0000000..f809512 --- /dev/null +++ b/gae/webapp/src/endpoint/build_info_test.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src.endpoint import build_info +from webapp.src.proto import model +from webapp.src.testing import unittest_base + + +class BuildInfoTest(unittest_base.UnitTestBase): + """A class to test build_info endpoint API.""" + + def setUp(self): + """Initializes test""" + super(BuildInfoTest, self).setUp() + + def testSetNewBuildModel(self): + """Asserts build_info/set API receives a new build.""" + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 0) + container = ( + build_info.BUILD_INFO_RESOURCE.combined_message_class( + manifest_branch=self.GetRandomString(), + build_id=self.GetRandomString(), + build_target=self.GetRandomString(), + build_type=self.GetRandomString(), + artifact_type=self.GetRandomString(), + )) + api = build_info.BuildInfoApi() + response = api.set(container) + + self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 1) + + def testSetDuplicatedBuildModel(self): + """Asserts build_info/set API receives a duplicated build.""" + manifest_branch = self.GetRandomString() + build_id = self.GetRandomString() + build_target = self.GetRandomString() + build_type = self.GetRandomString() + artifact_type = self.GetRandomString() + + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 0) + container = ( + build_info.BUILD_INFO_RESOURCE.combined_message_class( + manifest_branch=manifest_branch, + build_id=build_id, + build_target=build_target, + build_type=build_type, + artifact_type=artifact_type, + )) + api = build_info.BuildInfoApi() + response = api.set(container) + + self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 1) + + container = ( + build_info.BUILD_INFO_RESOURCE.combined_message_class( + manifest_branch=manifest_branch, + build_id=build_id, + build_target=build_target, + build_type=build_type, + artifact_type=artifact_type, + )) + api = build_info.BuildInfoApi() + response = api.set(container) + self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 1) + + def testUpdateSignedBuildModel(self): + """Asserts build_info/set API receives a duplicated build.""" + manifest_branch = self.GetRandomString() + build_id = self.GetRandomString() + build_target = self.GetRandomString() + build_type = self.GetRandomString() + artifact_type = self.GetRandomString() + + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 0) + container = ( + build_info.BUILD_INFO_RESOURCE.combined_message_class( + manifest_branch=manifest_branch, + build_id=build_id, + build_target=build_target, + build_type=build_type, + artifact_type=artifact_type, + signed=False, + )) + api = build_info.BuildInfoApi() + response = api.set(container) + + self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 1) + + container = ( + build_info.BUILD_INFO_RESOURCE.combined_message_class( + manifest_branch=manifest_branch, + build_id=build_id, + build_target=build_target, + build_type=build_type, + artifact_type=artifact_type, + signed=True + )) + api = build_info.BuildInfoApi() + response = api.set(container) + self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + builds = model.BuildModel.query().fetch() + self.assertEquals(len(builds), 1) + self.assertEquals(builds[0].signed, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py index 47a7bef..f243af1 100644 --- a/gae/webapp/src/endpoint/endpoint_base.py +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -76,12 +76,14 @@ class EndpointBase(remote.Service): if isinstance(value, messages.Message): attrs = [ x.name for x in value.all_fields() - if not assigned_only or value.get_assigned_value(x.name) + if not assigned_only or ( + value.get_assigned_value(x.name) not in [None, []]) ] elif isinstance(value, ndb.Model): attrs = [ x for x in list(value.to_dict()) - if not assigned_only or getattr(value, x, None) + if not assigned_only or ( + getattr(value, x, None) not in [None, []]) ] else: raise ValueError("Only protorpc.messages.Message or ndb.Model " diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 9f7eb2f..c12188b 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -51,61 +51,15 @@ class JobQueueApi(endpoint_base.EndpointBase): existing_jobs, key=lambda x: (Status.GetPriorityValue(x.priority), x.timestamp)) - job_message = model.JobMessage() - job_message.hostname = "" - job_message.priority = "" - job_message.test_name = "" - job_message.require_signed_device_build = False - job_message.device = "" - job_message.serial = [""] - job_message.manifest_branch = "" - job_message.build_target = "" - job_message.shards = 0 - job_message.param = [""] - job_message.build_id = "" - job_message.status = 0 - job_message.period = 0 - job_message.retry_count = 0 - if priority_sorted_jobs: job = priority_sorted_jobs[0] job.status = Status.JOB_STATUS_DICT["leased"] job.put() - job_message.hostname = job.hostname - job_message.priority = job.priority - job_message.test_name = job.test_name - job_message.require_signed_device_build = ( - job.require_signed_device_build) - job_message.device = job.device - job_message.serial = job.serial - job_message.build_storage_type = job.build_storage_type - job_message.manifest_branch = job.manifest_branch - job_message.build_target = job.build_target - job_message.shards = job.shards - job_message.param = job.param - job_message.build_id = job.build_id - job_message.pab_account_id = job.pab_account_id - job_message.status = job.status - job_message.period = job.period - job_message.retry_count = job.retry_count - job_message.gsi_storage_type = job.gsi_storage_type - job_message.gsi_branch = job.gsi_branch - job_message.gsi_build_target = job.gsi_build_target - job_message.gsi_build_id = job.gsi_build_id - job_message.gsi_pab_account_id = job.gsi_pab_account_id - job_message.gsi_vendor_version = job.gsi_vendor_version - job_message.test_storage_type = job.test_storage_type - job_message.test_branch = job.test_branch - job_message.test_build_target = job.test_build_target - job_message.test_build_id = job.test_build_id - job_message.test_pab_account_id = job.test_pab_account_id - job_message.test_type = job.test_type - job_message.image_package_repo_base = job.image_package_repo_base - job_message.has_bootloader_img = job.has_bootloader_img - job_message.has_radio_img = job.has_radio_img - job_message.report_bucket = job.report_bucket - job_message.report_spreadsheet_id = job.report_spreadsheet_id + job_message = model.JobMessage() + common_attributes = self.GetCommonAttributes(job, model.JobMessage) + for attr in common_attributes: + setattr(job_message, attr, getattr(job, attr)) device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) @@ -120,7 +74,7 @@ class JobQueueApi(endpoint_base.EndpointBase): jobs=[job_message]) else: return model.JobLeaseResponse( - return_code=model.ReturnCodeMessage.FAIL, jobs=[job_message]) + return_code=model.ReturnCodeMessage.FAIL, jobs=[]) @endpoints.method( JOB_QUEUE_RESOURCE, @@ -142,9 +96,6 @@ class JobQueueApi(endpoint_base.EndpointBase): x for x in existing_jobs if set(x.serial) == set(request.serial) ] - job_message = model.JobMessage() - job_messages = [] - if len(same_jobs) > 1: logging.warning("[heartbeat] more than one job is found!") logging.warning( @@ -155,23 +106,11 @@ class JobQueueApi(endpoint_base.EndpointBase): if same_jobs: job = same_jobs[0] - job_message.hostname = job.hostname - job_message.priority = job.priority - job_message.test_name = job.test_name - job_message.require_signed_device_build = ( - job.require_signed_device_build) - job_message.device = job.device - job_message.serial = job.serial - job_message.manifest_branch = job.manifest_branch - job_message.build_target = job.build_target - job_message.shards = job.shards - job_message.param = job.param - job_message.build_id = job.build_id - job_message.status = job.status - job_message.period = job.period - job_message.retry_count = job.retry_count - job_message.test_type = job.test_type - job_messages.append(job_message) + job_message = model.JobMessage() + common_attributes = self.GetCommonAttributes(job, model.JobMessage) + for attr in common_attributes: + setattr(job_message, attr, getattr(job, attr)) + device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) devices = device_query.fetch() @@ -222,7 +161,7 @@ class JobQueueApi(endpoint_base.EndpointBase): job.put() model_util.UpdateParentSchedule(job, request.status) return model.JobLeaseResponse( - return_code=model.ReturnCodeMessage.SUCCESS, jobs=job_messages) + return_code=model.ReturnCodeMessage.SUCCESS, jobs=[job_message]) return model.JobLeaseResponse( - return_code=model.ReturnCodeMessage.FAIL, jobs=job_messages) + return_code=model.ReturnCodeMessage.FAIL, jobs=[]) diff --git a/gae/webapp/src/endpoint/job_queue_test.py b/gae/webapp/src/endpoint/job_queue_test.py new file mode 100644 index 0000000..5e0e485 --- /dev/null +++ b/gae/webapp/src/endpoint/job_queue_test.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import datetime +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.endpoint import job_queue +from webapp.src.proto import model +from webapp.src.testing import unittest_base + + +class JobQueueTest(unittest_base.UnitTestBase): + """A class to test job_queue endpoint API.""" + + def setUp(self): + """Initializes test""" + super(JobQueueTest, self).setUp() + + def testGetJobModel(self): + """Asserts job_queue/get API receives a job lease request.""" + test_values = { + "test_type": Status.TEST_TYPE_DICT[Status.TEST_TYPE_TOT], + "hostname": self.GetRandomString(), + "priority": self.GetRandomString(), + "test_name": self.GetRandomString(), + "require_signed_device_build": False, + "has_bootloader_img": True, + "has_radio_img": False, + "device": self.GetRandomString(), + "serial": ["serial01", "serial02"], + "build_storage_type": Status.STORAGE_TYPE_DICT["GCS"], + "manifest_branch": self.GetRandomString(), + "build_target": self.GetRandomString(), + "build_id": self.GetRandomString(), + "pab_account_id": self.GetRandomString(), + "shards": 1, + "param": [""], + "status": Status.JOB_STATUS_DICT["ready"], + "period": 360, + "gsi_storage_type": Status.STORAGE_TYPE_DICT["GCS"], + "gsi_branch": self.GetRandomString(), + "gsi_build_target": self.GetRandomString(), + "gsi_build_id": self.GetRandomString(), + "gsi_pab_account_id": self.GetRandomString(), + "gsi_vendor_version": self.GetRandomString(), + "test_storage_type": Status.STORAGE_TYPE_DICT["GCS"], + "test_branch": self.GetRandomString(), + "test_build_target": self.GetRandomString(), + "test_build_id": self.GetRandomString(), + "test_pab_account_id": self.GetRandomString(), + "retry_count": 2, + "infra_log_url": self.GetRandomString(), + "image_package_repo_base": self.GetRandomString(), + } + + for serial in test_values["serial"]: + self.GenerateDeviceModel(serial=serial).put() + + job = model.JobModel() + for key in test_values: + setattr(job, key, test_values[key]) + job.timestamp = datetime.datetime.now() + job.put() + + container = (job_queue.JOB_QUEUE_RESOURCE.combined_message_class( + hostname=test_values["hostname"])) + api = job_queue.JobQueueApi() + response = api.get(container) + + self.assertEquals(response.return_code, + model.ReturnCodeMessage.SUCCESS) + self.assertEquals(len(response.jobs), 1) + for key in test_values: + if key is "status": + self.assertEquals( + getattr(response.jobs[0], key), + Status.JOB_STATUS_DICT["leased"]) + else: + self.assertEquals( + getattr(response.jobs[0], key), test_values[key]) + + devices = model.DeviceModel.query().fetch() + for device in devices: + self.assertEquals(device.scheduling_status, + Status.DEVICE_SCHEDULING_STATUS_DICT["use"]) + + # test job heartbeat api + container = (job_queue.JOB_QUEUE_RESOURCE.combined_message_class( + hostname=response.jobs[0].hostname, + manifest_branch=response.jobs[0].manifest_branch, + build_target=response.jobs[0].build_target, + test_name=response.jobs[0].test_name, + serial=response.jobs[0].serial, + status=response.jobs[0].status, + )) + api = job_queue.JobQueueApi() + response = api.heartbeat(container) + self.assertEquals(response.return_code, + model.ReturnCodeMessage.SUCCESS) + + jobs = model.JobModel.query().fetch() + self.assertEquals(len(jobs), 1) + self.assertEquals(jobs[0].status, Status.JOB_STATUS_DICT["leased"]) + self.assertTrue(datetime.datetime.now() - jobs[0].heartbeat_stamp < + datetime.timedelta(seconds=1)) + + # test job heartbeat api to complete the job + container = (job_queue.JOB_QUEUE_RESOURCE.combined_message_class( + hostname=response.jobs[0].hostname, + manifest_branch=response.jobs[0].manifest_branch, + build_target=response.jobs[0].build_target, + test_name=response.jobs[0].test_name, + serial=response.jobs[0].serial, + status=Status.JOB_STATUS_DICT["complete"], + )) + api = job_queue.JobQueueApi() + response = api.heartbeat(container) + self.assertEquals(response.return_code, + model.ReturnCodeMessage.SUCCESS) + + jobs = model.JobModel.query().fetch() + self.assertEquals(len(jobs), 1) + self.assertEquals(jobs[0].status, Status.JOB_STATUS_DICT["complete"]) + self.assertTrue(datetime.datetime.now() - jobs[0].heartbeat_stamp < + datetime.timedelta(seconds=1)) + + devices = model.DeviceModel.query().fetch() + for device in devices: + self.assertEquals(device.scheduling_status, + Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/gae/webapp/src/endpoint/schedule_info_test.py b/gae/webapp/src/endpoint/schedule_info_test.py index 82df829..811a421 100644 --- a/gae/webapp/src/endpoint/schedule_info_test.py +++ b/gae/webapp/src/endpoint/schedule_info_test.py @@ -29,11 +29,7 @@ from webapp.src.testing import unittest_base class ScheduleInfoTest(unittest_base.UnitTestBase): - """A class to test schedule_info endpoint API. - - Attributes: - scheduler: A mock schedule_worker.ScheduleHandler. - """ + """A class to test schedule_info endpoint API.""" def setUp(self): """Initializes test""" @@ -74,7 +70,7 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): api = schedule_info.ScheduleInfoApi() response = api.set(container) - self.assertTrue(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) def testSetWithEmptyRepeatedField(self): """Asserts schedule_info/set API receives a message. @@ -115,7 +111,7 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): api = schedule_info.ScheduleInfoApi() response = api.set(container) - self.assertTrue(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) if __name__ == "__main__": -- cgit v1.2.3 From e4cf830844fb040ee8cd72a04bda1be7113a012d Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 24 Jul 2018 14:01:09 +0900 Subject: Fix SelectTargetLab() to return correct value. Test: mma Bug: 111486439 Change-Id: If31880328fc5b5abaea3e6237cc6351de3304f2a --- gae/webapp/src/scheduler/schedule_worker.py | 10 ++++------ gae/webapp/src/scheduler/schedule_worker_test.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 4b98b37..ea32cc9 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -470,7 +470,7 @@ class ScheduleHandler(webapp2.RequestHandler): host_devices.sort( key=lambda x: (len(x.device_equipment) if x.device_equipment else 0)) - available_devices.append(host_devices) + available_devices.append((host_devices, target_device)) self.logger.Unindent() self.logger.Unindent() @@ -480,12 +480,10 @@ class ScheduleHandler(webapp2.RequestHandler): return None, None, [] available_devices.sort(key=lambda x: ( - sum([len(y.device_equipment) for y in x[:schedule.shards]]))) + sum([len(y.device_equipment) for y in x[0][:schedule.shards]]))) selected_host_devices = available_devices[0] - return selected_host_devices[0].hostname, selected_host_devices[ - 0].product, [ - x.serial for x in selected_host_devices[:schedule.shards] - ] + return selected_host_devices[0][0].hostname, selected_host_devices[ + 1], [x.serial for x in selected_host_devices[0][:schedule.shards]] def GetProductName(self, schedule): """Gets a product name from schedule instance. diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index dfb592f..fad80fe 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -499,6 +499,25 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): for job_device in jobs[0].serial: self.assertIn(job_device, host_a_devices_serial) + def testSelectTargetLab(self): + """Asserts SelectTargetLab() method.""" + lab = self.GenerateLabModel() + lab.put() + + device = self.GenerateDeviceModel(hostname=lab.hostname) + device.put() + + schedule = self.GenerateScheduleModel(device_model=device, + lab_model=lab) + schedule.put() + + ret_host, ret_device, ret_serials = ( + self.scheduler.SelectTargetLab(schedule)) + + self.assertEqual(lab.hostname, ret_host) + self.assertEqual("{}/{}".format(lab.name, device.product), ret_device) + self.assertEqual([device.serial], ret_serials) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From e6fbec0d2c0893bd6ab8a81d8405b563d91ea3bc Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 26 Jul 2018 11:53:35 +0900 Subject: Remove unnecessary files and comments. Test: ng serve Bug: 74575555 --- gae/frontend/README.md | 27 ---------- gae/frontend/src/assets/.gitkeep | 0 gae/frontend/src/browserslist | 6 +-- gae/frontend/src/environments/environment.ts | 12 ----- gae/frontend/src/polyfills.ts | 81 ++-------------------------- gae/frontend/src/styles.css | 1 - 6 files changed, 4 insertions(+), 123 deletions(-) delete mode 100644 gae/frontend/README.md delete mode 100644 gae/frontend/src/assets/.gitkeep diff --git a/gae/frontend/README.md b/gae/frontend/README.md deleted file mode 100644 index 4217895..0000000 --- a/gae/frontend/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Frontend - -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.8. - -## Development server - -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. - -## Code scaffolding - -Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. - -## Build - -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. - -## Running unit tests - -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). - -## Running end-to-end tests - -Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). - -## Further help - -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/gae/frontend/src/assets/.gitkeep b/gae/frontend/src/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/gae/frontend/src/browserslist b/gae/frontend/src/browserslist index 8e09ab4..3206b4e 100644 --- a/gae/frontend/src/browserslist +++ b/gae/frontend/src/browserslist @@ -1,9 +1,5 @@ -# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries -# For IE 9-11 support, please uncomment the last line of the file and adjust as needed +# For autoprefixer to adjust CSS to support the below specified browsers > 0.5% last 2 versions Firefox ESR not dead -# IE 9-11 \ No newline at end of file diff --git a/gae/frontend/src/environments/environment.ts b/gae/frontend/src/environments/environment.ts index 012182e..ffe8aed 100644 --- a/gae/frontend/src/environments/environment.ts +++ b/gae/frontend/src/environments/environment.ts @@ -1,15 +1,3 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - export const environment = { production: false }; - -/* - * In development mode, to ignore zone related error stack frames such as - * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can - * import the following file, but please comment it out in production mode - * because it will have performance impact when throw error - */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/gae/frontend/src/polyfills.ts b/gae/frontend/src/polyfills.ts index d310405..25a0787 100644 --- a/gae/frontend/src/polyfills.ts +++ b/gae/frontend/src/polyfills.ts @@ -1,80 +1,5 @@ -/** - * This file includes polyfills needed by Angular and is loaded before the app. - * You can add your own extra polyfills to this file. - * - * This file is divided into 2 sections: - * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. - * 2. Application imports. Files imported after ZoneJS that should be loaded before your main - * file. - * - * The current setup is for so-called "evergreen" browsers; the last versions of browsers that - * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), - * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. - * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html - */ - -/*************************************************************************************************** - * BROWSER POLYFILLS - */ - -/** IE9, IE10 and IE11 requires all of the following polyfills. **/ -// import 'core-js/es6/symbol'; -// import 'core-js/es6/object'; -// import 'core-js/es6/function'; -// import 'core-js/es6/parse-int'; -// import 'core-js/es6/parse-float'; -// import 'core-js/es6/number'; -// import 'core-js/es6/math'; -// import 'core-js/es6/string'; -// import 'core-js/es6/date'; -// import 'core-js/es6/array'; -// import 'core-js/es6/regexp'; -// import 'core-js/es6/map'; -// import 'core-js/es6/weak-map'; -// import 'core-js/es6/set'; - -/** IE10 and IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** IE10 and IE11 requires the following for the Reflect API. */ -// import 'core-js/es6/reflect'; - - -/** Evergreen browsers require these. **/ -// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +// Evergreen browsers require these. import 'core-js/es7/reflect'; - -/** - * Web Animations `@angular/platform-browser/animations` - * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. - * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - **/ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - -/** - * By default, zone.js will patch all possible macroTask and DomEvents - * user can disable parts of macroTask/DomEvents patch by setting following flags - */ - - // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - - /* - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - */ -// (window as any).__Zone_enable_cross_context_check = true; - -/*************************************************************************************************** - * Zone JS is required by default for Angular itself. - */ -import 'zone.js/dist/zone'; // Included with Angular CLI. - - - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ +// Zone JS is required by default for Angular itself. +import 'zone.js/dist/zone'; diff --git a/gae/frontend/src/styles.css b/gae/frontend/src/styles.css index 90d4ee0..e69de29 100644 --- a/gae/frontend/src/styles.css +++ b/gae/frontend/src/styles.css @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ -- cgit v1.2.3 From 752619e2d63c136252c9c74255e670ea5d88bddb Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 2 Aug 2018 15:50:33 +0900 Subject: Use ndb.put_multi() when inserting multi entities. Test: python testing/e2e_test.py, mma Bug: 77298544 Change-Id: Ia1a868235554dae6a4f53aa05bcc343dc77bd4d7 --- gae/webapp/src/dashboard/job_list.py | 8 +++++++- gae/webapp/src/endpoint/host_info.py | 11 +++++++++-- gae/webapp/src/endpoint/job_queue.py | 16 ++++++++++++---- gae/webapp/src/endpoint/lab_info.py | 11 +++++++++-- gae/webapp/src/scheduler/schedule_worker.py | 5 ++++- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py index 5488c64..6408367 100644 --- a/gae/webapp/src/dashboard/job_list.py +++ b/gae/webapp/src/dashboard/job_list.py @@ -22,6 +22,8 @@ from webapp.src.handlers import base from webapp.src.scheduler import schedule_worker from webapp.src.proto import model +from google.appengine.ext import ndb + def test_type_text(test_type, join_str=", "): """Generates text to represent in HTML with given test type. @@ -172,10 +174,14 @@ class CreateJobPage(JobBase): message = "Can't create a job because at some devices " \ "are not available (%s)." % error_devices else: + devices_to_put = [] for device in devices: device.scheduling_status = ( vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["reserved"]) - device.put() + devices_to_put.append(device) + if devices_to_put: + ndb.put_multi(devices_to_put) + message = "A new job is created!" new_job = model.JobModel() diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index 027f163..f851bb9 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -17,6 +17,7 @@ import datetime import endpoints from google.appengine.api import users +from google.appengine.ext import ndb from webapp.src import vtslab_status as Status from webapp.src.endpoint import endpoint_base @@ -43,6 +44,7 @@ def AddNullDevices(hostname, null_device_count): existing_null_device_count = len(null_devices) if existing_null_device_count < null_device_count: + devices_to_put = [] for _ in range(null_device_count - existing_null_device_count): device = model.DeviceModel() device.hostname = hostname @@ -52,7 +54,9 @@ def AddNullDevices(hostname, null_device_count): device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ "free"] device.timestamp = datetime.datetime.now() - device.put() + devices_to_put.append(device) + if devices_to_put: + ndb.put_multi(devices_to_put) @endpoints.api(name='host_info', version='v1') @@ -72,6 +76,7 @@ class HostInfoApi(endpoint_base.EndpointBase): else: username = "anonymous" + devices_to_put = [] for request_device in request.devices: device_query = model.DeviceModel.query( model.DeviceModel.serial == request_device.serial @@ -89,7 +94,9 @@ class HostInfoApi(endpoint_base.EndpointBase): device.product = request_device.product device.status = request_device.status device.timestamp = datetime.datetime.now() - device.put() + devices_to_put.append(device) + if devices_to_put: + ndb.put_multi(devices_to_put) return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index c12188b..5806345 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -24,6 +24,8 @@ from webapp.src.proto import model from webapp.src.utils import email_util from webapp.src.utils import model_util +from google.appengine.ext import ndb + JOB_QUEUE_RESOURCE = endpoints.ResourceContainer(model.JobMessage) GCS_URL_PREFIX = "gs://" HTTP_HTTPS_REGEX = "^https?://" @@ -64,10 +66,13 @@ class JobQueueApi(endpoint_base.EndpointBase): device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(job.serial)) devices = device_query.fetch() + devices_to_put = [] for device in devices: device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ "use"] - device.put() + devices_to_put.append(device) + if devices_to_put: + ndb.put_multi(devices_to_put) return model.JobLeaseResponse( return_code=model.ReturnCodeMessage.SUCCESS, @@ -121,12 +126,13 @@ class JobQueueApi(endpoint_base.EndpointBase): request.status)) logging.debug("[heartbeat] - devices = {}".format( ", ".join([device.serial for device in devices]))) + devices_to_put = [] if request.status == Status.JOB_STATUS_DICT["complete"]: job.status = request.status for device in devices: device.scheduling_status = ( Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) - device.put() + devices_to_put.append(device) elif (request.status in [Status.JOB_STATUS_DICT["infra-err"], Status.JOB_STATUS_DICT["bootup-err"]]): job.status = request.status @@ -135,16 +141,18 @@ class JobQueueApi(endpoint_base.EndpointBase): device.scheduling_status = ( Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) device.status = Status.DEVICE_STATUS_DICT["unknown"] - device.put() + devices_to_put.append(device) elif request.status == Status.JOB_STATUS_DICT["leased"]: job.status = request.status for device in devices: device.timestamp = datetime.datetime.now() - device.put() + devices_to_put.append(device) else: logging.error( "[heartbeat] Unexpected job status is received. - {}". format(request.serial)) + if devices_to_put: + ndb.put_multi(devices_to_put) if request.infra_log_url: if request.infra_log_url.startswith(GCS_URL_PREFIX): diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 215716d..caf466d 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -57,6 +57,7 @@ class LabInfoApi(endpoint_base.EndpointBase): def set(self, request): """Sets the lab info based on `request`.""" if "host" in [x.name for x in request.all_fields()]: + labs_to_put = [] for host in request.host: duplicate_query = model.LabModel.query( model.LabModel.name == request.name, @@ -120,11 +121,14 @@ class LabInfoApi(endpoint_base.EndpointBase): ndb.put_multi(devices_to_put) lab.timestamp = datetime.datetime.now() - lab.put() + labs_to_put.append(lab) if null_device_count > 0: host_info.AddNullDevices(host.hostname, null_device_count) + if labs_to_put: + ndb.put_multi(labs_to_put) + return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) @@ -140,9 +144,12 @@ class LabInfoApi(endpoint_base.EndpointBase): model.LabModel.hostname == request.hostname) labs = lab_query.fetch() + labs_to_put = [] for lab in labs: lab.vtslab_version = request.vtslab_version.split(":")[0] - lab.put() + labs_to_put.append(lab) + if labs_to_put: + ndb.put_multi(labs_to_put) return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index ea32cc9..6e1a865 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -131,10 +131,13 @@ class ScheduleHandler(webapp2.RequestHandler): device_query = model.DeviceModel.query( model.DeviceModel.serial.IN(target_device_serials)) devices = device_query.fetch() + devices_to_put = [] for device in devices: device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ "reserved"] - device.put() + devices_to_put.append(device) + if devices_to_put: + ndb.put_multi(devices_to_put) def FindBuildId(self, artifact_type, manifest_branch, target, signed=False): -- cgit v1.2.3 From fe515025a61acf62fb526b2ffb944597b65831e1 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 2 Aug 2018 18:08:30 +0900 Subject: Prevent creating duplicated DeviceModel. Test: python testing/e2e_test.py Bug: 112121209 Change-Id: Id178fe07dd1cf82a6a04128ee33b2cc2df9d348c --- gae/webapp/src/endpoint/host_info.py | 4 +- gae/webapp/src/endpoint/host_info_test.py | 93 ++++++++++++++++++++ gae/webapp/src/endpoint/lab_info.py | 3 +- gae/webapp/src/endpoint/lab_info_test.py | 137 ++++++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 gae/webapp/src/endpoint/host_info_test.py create mode 100644 gae/webapp/src/endpoint/lab_info_test.py diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index 027f163..8d77a38 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -84,9 +84,11 @@ class HostInfoApi(endpoint_base.EndpointBase): device.serial = request_device.serial device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ "free"] + if not device.product or request_device.product is not "error": + device.product = request_device.product + device.username = username device.hostname = request.hostname - device.product = request_device.product device.status = request_device.status device.timestamp = datetime.datetime.now() device.put() diff --git a/gae/webapp/src/endpoint/host_info_test.py b/gae/webapp/src/endpoint/host_info_test.py new file mode 100644 index 0000000..e41037c --- /dev/null +++ b/gae/webapp/src/endpoint/host_info_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src import vtslab_status as Status +from webapp.src.endpoint import host_info +from webapp.src.proto import model +from webapp.src.testing import unittest_base + + +class HostInfoTest(unittest_base.UnitTestBase): + """A class to test host_info endpoint API.""" + + def setUp(self): + """Initializes test""" + super(HostInfoTest, self).setUp() + + + def testUpdateExistingDevice(self): + """Asserts that device update does not create a duplicate.""" + hostname = self.GetRandomString() + serial = self.GetRandomString() + product = self.GetRandomString() + error_device = { + "serial": serial, + "product": "error", + } + container = ( + host_info.HOST_INFO_RESOURCE.combined_message_class( + hostname=hostname, + devices=[error_device], + )) + + api = host_info.HostInfoApi() + api.set(container) + + devices = model.DeviceModel.query().fetch() + self.assertEqual(len(devices), 1) + + # name "error" is allowed as initial name. + self.assertEqual(devices[0].product, "error") + + correct_device = { + "serial": serial, + "product": product, + } + container = ( + host_info.HOST_INFO_RESOURCE.combined_message_class( + hostname=hostname, + devices=[correct_device], + )) + api.set(container) + + devices = model.DeviceModel.query().fetch() + self.assertEqual(len(devices), 1) + # correct product name (which is not "error") should be overwritten. + self.assertEqual(devices[0].product, product) + + container = ( + host_info.HOST_INFO_RESOURCE.combined_message_class( + hostname=hostname, + devices=[error_device], + )) + api.set(container) + + devices = model.DeviceModel.query().fetch() + self.assertEqual(len(devices), 1) + # "error" should be ignored. + self.assertEqual(devices[0].product, product) + + +if __name__ == "__main__": + unittest.main() diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 215716d..2564a76 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -82,8 +82,7 @@ class LabInfoApi(endpoint_base.EndpointBase): continue if config_device.serial and config_device.product: device_query = model.DeviceModel.query( - model.DeviceModel.serial == config_device.serial, - model.DeviceModel.product == config_device.product) + model.DeviceModel.serial == config_device.serial) devices = device_query.fetch() if devices: device = devices[0] diff --git a/gae/webapp/src/endpoint/lab_info_test.py b/gae/webapp/src/endpoint/lab_info_test.py new file mode 100644 index 0000000..7320c7b --- /dev/null +++ b/gae/webapp/src/endpoint/lab_info_test.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from webapp.src.endpoint import lab_info +from webapp.src.proto import model +from webapp.src.testing import unittest_base + + +class LabInfoTest(unittest_base.UnitTestBase): + """A class to test lab_info endpoint API.""" + + def setUp(self): + """Initializes test""" + super(LabInfoTest, self).setUp() + + def testUpdateErrorDevice(self): + """Asserts that device update does not create a duplicate.""" + device_serial = self.GetRandomString() + product = self.GetRandomString() + device_equipment = [self.GetRandomString()] + device_info = { + "serial": device_serial, + "product": product, + "device_equipment": device_equipment + } + + hostname = self.GetRandomString() + host_info = { + "hostname": hostname, + "ip": self.GetRandomString(), + "script": self.GetRandomString(), + "device": [device_info], + "vtslab_version": self.GetRandomString(), + "host_equipment": [], + } + + lab_name = self.GetRandomString() + container = ( + lab_info.LAB_INFO_RESOURCE.combined_message_class( + name=lab_name, + owner=self.GetRandomString(), + admin=[self.GetRandomString()], + host=[host_info], + )) + + api = lab_info.LabInfoApi() + api.set(container) + + devices = model.DeviceModel.query().fetch() + self.assertEqual(len(devices), 1) + self.assertEqual(devices[0].product, product) + + # change device product name. + devices[0].product = "error" + devices[0].put() + + api.set(container) + + devices = model.DeviceModel.query().fetch() + # there should not be duplicates. + self.assertEqual(len(devices), 1) + # stored device name should be kept. + self.assertEqual(devices[0].product, "error") + + + def testUpdateExistingDevice(self): + """Asserts that device update does not create a duplicate.""" + device_serial = self.GetRandomString() + product = self.GetRandomString() + device_equipment = [self.GetRandomString()] + device_info = { + "serial": device_serial, + "product": product, + "device_equipment": device_equipment, + } + + hostname = self.GetRandomString() + host_info = { + "hostname": hostname, + "ip": self.GetRandomString(), + "script": self.GetRandomString(), + "device": [device_info], + "vtslab_version": self.GetRandomString(), + "host_equipment": [], + } + + lab_name = self.GetRandomString() + container = ( + lab_info.LAB_INFO_RESOURCE.combined_message_class( + name=lab_name, + owner=self.GetRandomString(), + admin=[self.GetRandomString()], + host=[host_info], + )) + + device = self.GenerateDeviceModel(product="error", + serial=device_serial, + hostname=hostname) + device.put() + + api = lab_info.LabInfoApi() + api.set(container) + + devices = model.DeviceModel.query().fetch() + self.assertEqual(len(devices), 1) + + # stored device name should be kept. + self.assertEqual(devices[0].product, "error") + + # device equipment should be updated. + self.assertEqual(set(devices[0].device_equipment), + set(device_equipment)) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 304af0a40bb4e0284baf94edf4f5a16f0e9d78df Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Fri, 3 Aug 2018 14:40:50 +0900 Subject: Replace assertEquals() with assertEqual(). As a python documentation described, assertEquals is deprecated and replaced with assertEqual. https://docs.python.org/2/library/unittest.html#deprecated-aliases Test: python testing/e2e_test.py Bug: 77617865 --- gae/webapp/src/endpoint/build_info_test.py | 28 ++++++++++++------------ gae/webapp/src/endpoint/endpoint_base_test.py | 4 ++-- gae/webapp/src/endpoint/job_queue_test.py | 24 ++++++++++---------- gae/webapp/src/endpoint/schedule_info_test.py | 4 ++-- gae/webapp/src/scheduler/job_heartbeat_test.py | 4 ++-- gae/webapp/src/scheduler/schedule_worker_test.py | 2 +- gae/webapp/src/utils/model_util_test.py | 6 ++--- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/gae/webapp/src/endpoint/build_info_test.py b/gae/webapp/src/endpoint/build_info_test.py index f809512..8b70831 100644 --- a/gae/webapp/src/endpoint/build_info_test.py +++ b/gae/webapp/src/endpoint/build_info_test.py @@ -37,7 +37,7 @@ class BuildInfoTest(unittest_base.UnitTestBase): def testSetNewBuildModel(self): """Asserts build_info/set API receives a new build.""" builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 0) + self.assertEqual(len(builds), 0) container = ( build_info.BUILD_INFO_RESOURCE.combined_message_class( manifest_branch=self.GetRandomString(), @@ -49,9 +49,9 @@ class BuildInfoTest(unittest_base.UnitTestBase): api = build_info.BuildInfoApi() response = api.set(container) - self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 1) + self.assertEqual(len(builds), 1) def testSetDuplicatedBuildModel(self): """Asserts build_info/set API receives a duplicated build.""" @@ -62,7 +62,7 @@ class BuildInfoTest(unittest_base.UnitTestBase): artifact_type = self.GetRandomString() builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 0) + self.assertEqual(len(builds), 0) container = ( build_info.BUILD_INFO_RESOURCE.combined_message_class( manifest_branch=manifest_branch, @@ -74,9 +74,9 @@ class BuildInfoTest(unittest_base.UnitTestBase): api = build_info.BuildInfoApi() response = api.set(container) - self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 1) + self.assertEqual(len(builds), 1) container = ( build_info.BUILD_INFO_RESOURCE.combined_message_class( @@ -88,9 +88,9 @@ class BuildInfoTest(unittest_base.UnitTestBase): )) api = build_info.BuildInfoApi() response = api.set(container) - self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 1) + self.assertEqual(len(builds), 1) def testUpdateSignedBuildModel(self): """Asserts build_info/set API receives a duplicated build.""" @@ -101,7 +101,7 @@ class BuildInfoTest(unittest_base.UnitTestBase): artifact_type = self.GetRandomString() builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 0) + self.assertEqual(len(builds), 0) container = ( build_info.BUILD_INFO_RESOURCE.combined_message_class( manifest_branch=manifest_branch, @@ -114,9 +114,9 @@ class BuildInfoTest(unittest_base.UnitTestBase): api = build_info.BuildInfoApi() response = api.set(container) - self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 1) + self.assertEqual(len(builds), 1) container = ( build_info.BUILD_INFO_RESOURCE.combined_message_class( @@ -129,10 +129,10 @@ class BuildInfoTest(unittest_base.UnitTestBase): )) api = build_info.BuildInfoApi() response = api.set(container) - self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) builds = model.BuildModel.query().fetch() - self.assertEquals(len(builds), 1) - self.assertEquals(builds[0].signed, True) + self.assertEqual(len(builds), 1) + self.assertEqual(builds[0].signed, True) if __name__ == "__main__": diff --git a/gae/webapp/src/endpoint/endpoint_base_test.py b/gae/webapp/src/endpoint/endpoint_base_test.py index a2c9fb7..1265b05 100644 --- a/gae/webapp/src/endpoint/endpoint_base_test.py +++ b/gae/webapp/src/endpoint/endpoint_base_test.py @@ -41,7 +41,7 @@ class EndpointBaseTest(unittest_base.UnitTestBase): setattr(job_message, attr, attr) eb = endpoint_base.EndpointBase() result = eb.GetAttributes(job_message, assigned_only=True) - self.assertEquals(set(attrs), set(result)) + self.assertEqual(set(attrs), set(result)) def testGetAssignedModelAttributes(self): attrs = ["hostname", "priority", "test_branch"] @@ -50,7 +50,7 @@ class EndpointBaseTest(unittest_base.UnitTestBase): setattr(job, attr, attr) eb = endpoint_base.EndpointBase() result = eb.GetAttributes(job, assigned_only=True) - self.assertEquals(set(attrs), set(result)) + self.assertEqual(set(attrs), set(result)) def testGetAllMessagesAttributes(self): attrs = ["hostname", "priority", "test_branch"] diff --git a/gae/webapp/src/endpoint/job_queue_test.py b/gae/webapp/src/endpoint/job_queue_test.py index 5e0e485..750777d 100644 --- a/gae/webapp/src/endpoint/job_queue_test.py +++ b/gae/webapp/src/endpoint/job_queue_test.py @@ -87,21 +87,21 @@ class JobQueueTest(unittest_base.UnitTestBase): api = job_queue.JobQueueApi() response = api.get(container) - self.assertEquals(response.return_code, + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) - self.assertEquals(len(response.jobs), 1) + self.assertEqual(len(response.jobs), 1) for key in test_values: if key is "status": - self.assertEquals( + self.assertEqual( getattr(response.jobs[0], key), Status.JOB_STATUS_DICT["leased"]) else: - self.assertEquals( + self.assertEqual( getattr(response.jobs[0], key), test_values[key]) devices = model.DeviceModel.query().fetch() for device in devices: - self.assertEquals(device.scheduling_status, + self.assertEqual(device.scheduling_status, Status.DEVICE_SCHEDULING_STATUS_DICT["use"]) # test job heartbeat api @@ -115,12 +115,12 @@ class JobQueueTest(unittest_base.UnitTestBase): )) api = job_queue.JobQueueApi() response = api.heartbeat(container) - self.assertEquals(response.return_code, + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) jobs = model.JobModel.query().fetch() - self.assertEquals(len(jobs), 1) - self.assertEquals(jobs[0].status, Status.JOB_STATUS_DICT["leased"]) + self.assertEqual(len(jobs), 1) + self.assertEqual(jobs[0].status, Status.JOB_STATUS_DICT["leased"]) self.assertTrue(datetime.datetime.now() - jobs[0].heartbeat_stamp < datetime.timedelta(seconds=1)) @@ -135,18 +135,18 @@ class JobQueueTest(unittest_base.UnitTestBase): )) api = job_queue.JobQueueApi() response = api.heartbeat(container) - self.assertEquals(response.return_code, + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) jobs = model.JobModel.query().fetch() - self.assertEquals(len(jobs), 1) - self.assertEquals(jobs[0].status, Status.JOB_STATUS_DICT["complete"]) + self.assertEqual(len(jobs), 1) + self.assertEqual(jobs[0].status, Status.JOB_STATUS_DICT["complete"]) self.assertTrue(datetime.datetime.now() - jobs[0].heartbeat_stamp < datetime.timedelta(seconds=1)) devices = model.DeviceModel.query().fetch() for device in devices: - self.assertEquals(device.scheduling_status, + self.assertEqual(device.scheduling_status, Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) diff --git a/gae/webapp/src/endpoint/schedule_info_test.py b/gae/webapp/src/endpoint/schedule_info_test.py index 811a421..b9d1708 100644 --- a/gae/webapp/src/endpoint/schedule_info_test.py +++ b/gae/webapp/src/endpoint/schedule_info_test.py @@ -70,7 +70,7 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): api = schedule_info.ScheduleInfoApi() response = api.set(container) - self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) def testSetWithEmptyRepeatedField(self): """Asserts schedule_info/set API receives a message. @@ -111,7 +111,7 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): api = schedule_info.ScheduleInfoApi() response = api.set(container) - self.assertEquals(response.return_code, model.ReturnCodeMessage.SUCCESS) + self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) if __name__ == "__main__": diff --git a/gae/webapp/src/scheduler/job_heartbeat_test.py b/gae/webapp/src/scheduler/job_heartbeat_test.py index 9728ed7..c9f56a5 100644 --- a/gae/webapp/src/scheduler/job_heartbeat_test.py +++ b/gae/webapp/src/scheduler/job_heartbeat_test.py @@ -111,14 +111,14 @@ class JobHeartbeatTest(unittest_base.UnitTestBase): infra_error_jobs = [ x for x in jobs if x.status == Status.JOB_STATUS_DICT["infra-err"] ] - self.assertEquals(len(infra_error_jobs), 1) + self.assertEqual(len(infra_error_jobs), 1) # job[0]'s devices should be changed to free scheduling status. serials = infra_error_jobs[0].serial devices = model.DeviceModel.query( model.DeviceModel.serial.IN(serials)).fetch() for device in devices: - self.assertEquals(device.scheduling_status, + self.assertEqual(device.scheduling_status, Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index fad80fe..f13a470 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -292,7 +292,7 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): any([job.test_name == schedule2_m.test_name for job in jobs])) # now schedule_1's priority value should be changed. - self.assertEquals(schedule1_l_original_priority_value - 1, + self.assertEqual(schedule1_l_original_priority_value - 1, schedule1_l.priority_value) def testRetryAfterBootupError(self): diff --git a/gae/webapp/src/utils/model_util_test.py b/gae/webapp/src/utils/model_util_test.py index c6180f9..4be54b1 100644 --- a/gae/webapp/src/utils/model_util_test.py +++ b/gae/webapp/src/utils/model_util_test.py @@ -142,7 +142,7 @@ class ModelTest(unittest_base.UnitTestBase): print("Asserting that job creation is blocked...") jobs = model.JobModel.query().fetch() - self.assertEquals(3, len(jobs)) + self.assertEqual(3, len(jobs)) for job in jobs: job.timestamp = datetime.datetime.now() - datetime.timedelta( @@ -153,7 +153,7 @@ class ModelTest(unittest_base.UnitTestBase): # a job should not be created. jobs = model.JobModel.query().fetch() - self.assertEquals(3, len(jobs)) + self.assertEqual(3, len(jobs)) print("Asserting that job creation is allowed after resuming...") schedule_from_db = model.ScheduleModel.query().fetch()[0] @@ -163,7 +163,7 @@ class ModelTest(unittest_base.UnitTestBase): scheduler.post() jobs = model.JobModel.query().fetch() - self.assertEquals(4, len(jobs)) + self.assertEqual(4, len(jobs)) if __name__ == "__main__": -- cgit v1.2.3 From 88fc519dd7583cb29732a18ff9407abbcc217aa6 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 6 Aug 2018 14:23:45 +0900 Subject: Update the version of dependencies. Test: ng install && ng serve Bug: 74575555 Change-Id: Ie82bd11444971bf15decbd5d40cd79831e489652 --- gae/frontend/package-lock.json | 230 +++++++++++++++++++++++------------------ gae/frontend/package.json | 31 +++--- 2 files changed, 148 insertions(+), 113 deletions(-) diff --git a/gae/frontend/package-lock.json b/gae/frontend/package-lock.json index ed44c54..f697726 100644 --- a/gae/frontend/package-lock.json +++ b/gae/frontend/package-lock.json @@ -25,7 +25,7 @@ "@angular-devkit/core": "0.6.8", "@ngtools/webpack": "6.0.8", "ajv": "6.4.0", - "autoprefixer": "8.6.3", + "autoprefixer": "8.6.2", "cache-loader": "1.2.2", "chalk": "2.2.2", "circular-dependency-plugin": "5.0.2", @@ -48,12 +48,12 @@ "opn": "5.3.0", "parse5": "4.0.0", "portfinder": "1.0.13", - "postcss": "6.0.23", + "postcss": "6.0.22", "postcss-import": "11.1.0", "postcss-loader": "2.1.5", "postcss-url": "7.3.2", "raw-loader": "0.5.1", - "resolve": "1.8.1", + "resolve": "1.7.1", "rxjs": "6.2.1", "sass-loader": "7.0.3", "silent-error": "1.1.0", @@ -63,7 +63,7 @@ "stylus": "0.54.5", "stylus-loader": "3.0.2", "tree-kill": "1.2.0", - "uglifyjs-webpack-plugin": "1.2.7", + "uglifyjs-webpack-plugin": "1.2.5", "url-loader": "1.0.1", "webpack": "4.8.3", "webpack-dev-middleware": "3.1.3", @@ -100,7 +100,7 @@ "dev": true, "requires": { "ajv": "6.4.0", - "chokidar": "2.0.4", + "chokidar": "2.0.3", "rxjs": "6.2.1", "source-map": "0.5.7" } @@ -120,7 +120,15 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.0.6.tgz", "integrity": "sha512-mJvWn0GuYARJfV9/KNUn5qUc5iNJKMSSNm//pRtUB8n829KnJHLnGpNsr95dzARH5wI3Om/t6hG3M0XCLbIfNQ==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" + } + }, + "@angular/cdk": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-6.2.1.tgz", + "integrity": "sha512-uwW4eIGJKqOkR+ew6YcEAh1J4SP98jdyDpsZ4IEMkV9+jXcKfcwcxGFpZvs9wJsAvAr8EgNmZ8h+iuZLwJsvmA==", + "requires": { + "tslib": "1.9.2" } }, "@angular/cli": { @@ -135,7 +143,7 @@ "@schematics/angular": "0.6.8", "@schematics/update": "0.6.8", "opn": "5.3.0", - "resolve": "1.8.1", + "resolve": "1.7.1", "rxjs": "6.2.1", "semver": "5.5.0", "silent-error": "1.1.0", @@ -165,7 +173,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.0.6.tgz", "integrity": "sha512-SjCrrGNJSeRMtNLv/ug5HpyRUexdNl11TrWCWMeu3ye3ss4k6EnuM9jGB196B0PIm0IbjO0KrpQ8bqBx0/2vqw==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "@angular/compiler": { @@ -173,7 +181,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.0.6.tgz", "integrity": "sha512-lcDNfkYLOWzOOqdD2Kspxwjk3xGs8kVLbq/8uk/aJ96ty8aA9j8Nbf3h53SCY9LuGoJMjOaaUpgwZCszFzqQyA==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "@angular/compiler-cli": { @@ -326,7 +334,15 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.0.6.tgz", "integrity": "sha512-7J4wuQ5Bss2GmCptyXSfmgWk/IbCFK/MJwaXOpADLB9iWOkOIvKRSTntb4l6j3OVd9boCbs6Z/xW/HT964iMvw==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" + } + }, + "@angular/flex-layout": { + "version": "6.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.16.tgz", + "integrity": "sha512-0AYtIBGrEJshdFMc6TXGloCkD19YTCRKVJl6xZHX4H5dLnUn+daqXcbh4UsWhayevnLp85HEf2ViHLmTa6jv3g==", + "requires": { + "tslib": "1.9.2" } }, "@angular/forms": { @@ -334,7 +350,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.0.6.tgz", "integrity": "sha512-uVcvUz8JzO/R6HtxIUtefjK55nf4gJt9WjVdnjmA66pQe1+aQYscyQu9QFykGfGqta/0luhVSU7J+5g0rIRr/g==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "@angular/http": { @@ -342,7 +358,7 @@ "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.0.6.tgz", "integrity": "sha512-ZyY7JS3lQM0HnKfoCJl+S9ZHeQVdG+FefjYE2s7pBKUufaoMo9DTIfQe5ZgSQeXRAFKjuUyJDf1EZlPVVvQzIw==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "@angular/language-service": { @@ -351,12 +367,20 @@ "integrity": "sha512-6zRuKreMPlLQkLGS7KaJ4xehwirPbst+S6tQZltcSHjgIKrZBu3acL7/tUo5G5jQW6OnPXWK9UYs2kCffPS3AQ==", "dev": true }, + "@angular/material": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-6.2.1.tgz", + "integrity": "sha512-SBoUXxHknkgwzp5pNDHW0jyrTM0d0Tk4lVyDbtEX8VEPtXqG5nL3BSgyjpJbTvqlmy2kOooUu3qgAmt87VH9lw==", + "requires": { + "tslib": "1.9.2" + } + }, "@angular/platform-browser": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.0.6.tgz", "integrity": "sha512-c+2c4Ba8IeIt9CnF1RmJVf/0xwljT9GSIJUC61SLrX01NMwRxDq/LC+tatcBGLzZ6rc1eYmsd1exTHOGfENOxw==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "@angular/platform-browser-dynamic": { @@ -364,7 +388,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.0.6.tgz", "integrity": "sha512-t5+dvfcwVaDa5H8qsVnPAvmNJa0rDwJMu1T6kfz8sAxzgiw6tOvIQShJX0Ka94+nPpd4mg7gv43VV705z6ryMA==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "@angular/router": { @@ -372,7 +396,7 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.0.6.tgz", "integrity": "sha512-R49Gh/ate//AloPGjtQ2Nl3HNMT21pumcUoWZEZtYw8UyTbxSKLMc40yzdsldGrKZ/G/CafFTaS1hpZD7MF5/w==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "@ngtools/webpack": { @@ -674,9 +698,9 @@ } }, "acorn": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", - "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.6.2.tgz", + "integrity": "sha512-zUzo1E5dI2Ey8+82egfnttyMlMZ2y0D8xOCO3PNPPlYXpl8NZvF6Qk9L9BEtJs+43FqEmfBViDqc5d1ckRDguw==", "dev": true }, "acorn-dynamic-import": { @@ -685,7 +709,7 @@ "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", "dev": true, "requires": { - "acorn": "5.7.1" + "acorn": "5.6.2" } }, "adm-zip": { @@ -787,9 +811,9 @@ } }, "app-root-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz", - "integrity": "sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", + "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", "dev": true }, "append-transform": { @@ -999,16 +1023,16 @@ "dev": true }, "autoprefixer": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.3.tgz", - "integrity": "sha512-KkQyCHBxma7R2eoEkjja/RHUBw+Fc1nY46LdV62fzJI5D7i8mLLCtAZ/AVR3UbXhDZ8mUz4C/PF4lZrbiHa1ZQ==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.2.tgz", + "integrity": "sha512-cv9v1mYYBcAnZq4MHseJ9AIdjQmNahnpCpPO46oTkQJS2GggsBp2azHjNpAuQ95Epvsg+AIsyjYhfI9YwFxGSA==", "dev": true, "requires": { "browserslist": "3.2.8", - "caniuse-lite": "1.0.30000858", + "caniuse-lite": "1.0.30000855", "normalize-range": "0.1.2", "num2fraction": "1.2.2", - "postcss": "6.0.23", + "postcss": "6.0.22", "postcss-value-parser": "3.3.0" } }, @@ -1488,8 +1512,8 @@ "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", "dev": true, "requires": { - "caniuse-lite": "1.0.30000858", - "electron-to-chromium": "1.3.50" + "caniuse-lite": "1.0.30000855", + "electron-to-chromium": "1.3.48" } }, "buffer": { @@ -1637,9 +1661,9 @@ } }, "caniuse-lite": { - "version": "1.0.30000858", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000858.tgz", - "integrity": "sha512-oJRGfVfwHr0VKcoy2UqIoRmQcDOugnNAQsWYI3/JTzExrlzxSKtmLW1N4h+gmjgpYCEJthHmaIjok894H5il/g==", + "version": "1.0.30000855", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000855.tgz", + "integrity": "sha512-ajORrkXa5UYk62P5PK6ZmBraYOAOr9HWy+XxLwjDg8Ys/5KiSyarg8tIA32ZVqbFhtz67wyySXnU9imkh2ZT2w==", "dev": true }, "caseless": { @@ -1688,9 +1712,9 @@ } }, "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", "dev": true, "requires": { "anymatch": "2.0.0", @@ -1701,7 +1725,6 @@ "inherits": "2.0.3", "is-binary-path": "1.0.1", "is-glob": "4.0.0", - "lodash.debounce": "4.0.8", "normalize-path": "2.1.1", "path-is-absolute": "1.0.1", "readdirp": "2.1.0", @@ -1825,7 +1848,7 @@ "integrity": "sha512-CKwfgpfkqi9dyzy4s6ELaxJ54QgJ6A8iTSsM4bzHbLuTpbKncvNc3DUlCvpnkHBhK47gEf4qFsWoYqLrJPhy6g==", "dev": true, "requires": { - "app-root-path": "2.1.0", + "app-root-path": "2.0.1", "css-selector-tokenizer": "0.7.0", "cssauron": "1.4.0", "semver-dsl": "1.0.1", @@ -2372,7 +2395,7 @@ "dev": true, "requires": { "foreach": "2.0.5", - "object-keys": "1.0.12" + "object-keys": "1.0.11" } }, "define-property": { @@ -2673,9 +2696,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.50", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.50.tgz", - "integrity": "sha1-dDi3b5K0G5GfP73TUPvQdX2s3fc=", + "version": "1.3.48", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz", + "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=", "dev": true }, "elliptic": { @@ -2839,9 +2862,9 @@ } }, "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", "dev": true, "requires": { "is-arrayish": "0.2.1" @@ -4323,7 +4346,7 @@ "array-union": "1.0.2", "dir-glob": "2.0.0", "glob": "7.1.2", - "ignore": "3.3.10", + "ignore": "3.3.8", "pify": "3.0.0", "slash": "1.0.0" } @@ -4573,9 +4596,9 @@ "dev": true }, "hosted-git-info": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.1.tgz", - "integrity": "sha512-Ba4+0M4YvIDUUsprMjhVTU1yN9F2/LJSAl69ZpzaLT4l4j5mwTS6jqqW9Ojvj6lKz/veqPzpJBqGbXspOb533A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", "dev": true }, "hpack.js": { @@ -4597,9 +4620,9 @@ "dev": true }, "html-minifier": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.17.tgz", - "integrity": "sha512-O+StuKL0UWfwX5Zv4rFxd60DPcT5DVjGq1AlnP6VQ8wzudft/W4hx5Wl98aSYNwFBHY6XWJreRw/BehX4l+diQ==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.16.tgz", + "integrity": "sha512-zP5EfLSpiLRp0aAgud4CQXPQZm9kXwWjR/cF0PfdOj+jjWnOaCgeZcll4kYXSvIBPeUMmyaSc7mM4IDtA+kboA==", "dev": true, "requires": { "camel-case": "3.0.0", @@ -4608,7 +4631,7 @@ "he": "1.1.1", "param-case": "2.1.1", "relateurl": "0.2.7", - "uglify-js": "3.4.2" + "uglify-js": "3.3.28" } }, "html-webpack-plugin": { @@ -4617,7 +4640,7 @@ "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { - "html-minifier": "3.5.17", + "html-minifier": "3.5.16", "loader-utils": "0.2.17", "lodash": "4.17.10", "pretty-error": "2.1.1", @@ -4791,9 +4814,9 @@ "dev": true }, "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", + "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==", "dev": true }, "image-size": { @@ -6047,12 +6070,6 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, "lodash.mergewith": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", @@ -6456,6 +6473,19 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, + "moment-timezone": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz", + "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==", + "requires": { + "moment": "2.22.2" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -6904,7 +6934,7 @@ "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", "dev": true, "requires": { - "hosted-git-info": "2.6.1", + "hosted-git-info": "2.6.0", "is-builtin-module": "1.0.0", "semver": "5.5.0", "validate-npm-package-license": "3.0.3" @@ -6931,7 +6961,7 @@ "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==", "dev": true, "requires": { - "hosted-git-info": "2.6.1", + "hosted-git-info": "2.6.0", "osenv": "0.1.5", "semver": "5.5.0", "validate-npm-package-name": "3.0.0" @@ -7055,9 +7085,9 @@ } }, "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", "dev": true }, "object-visit": { @@ -7078,7 +7108,7 @@ "define-properties": "1.1.2", "function-bind": "1.1.1", "has-symbols": "1.0.0", - "object-keys": "1.0.12" + "object-keys": "1.0.11" } }, "object.getownpropertydescriptors": { @@ -7355,7 +7385,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.2" + "error-ex": "1.3.1" } }, "parse5": { @@ -7527,9 +7557,9 @@ "dev": true }, "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "version": "6.0.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz", + "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==", "dev": true, "requires": { "chalk": "2.4.1", @@ -7562,10 +7592,10 @@ "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==", "dev": true, "requires": { - "postcss": "6.0.23", + "postcss": "6.0.22", "postcss-value-parser": "3.3.0", "read-cache": "1.0.0", - "resolve": "1.8.1" + "resolve": "1.7.1" } }, "postcss-load-config": { @@ -7607,7 +7637,7 @@ "dev": true, "requires": { "loader-utils": "1.1.0", - "postcss": "6.0.23", + "postcss": "6.0.22", "postcss-load-config": "1.2.0", "schema-utils": "0.4.5" } @@ -7621,7 +7651,7 @@ "mime": "1.6.0", "minimatch": "3.0.4", "mkdirp": "0.5.1", - "postcss": "6.0.23", + "postcss": "6.0.22", "xxhashjs": "0.2.2" } }, @@ -8291,9 +8321,9 @@ "dev": true }, "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", "dev": true, "requires": { "path-parse": "1.0.5" @@ -8375,7 +8405,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz", "integrity": "sha512-OwMxHxmnmHTUpgO+V7dZChf3Tixf4ih95cmXjzzadULziVl/FKhHScGLj4goEw9weePVOH2Q0+GcCBUhKCZc/g==", "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "safe-buffer": { @@ -9675,9 +9705,9 @@ } }, "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", + "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" }, "tslint": { "version": "5.9.1", @@ -9693,10 +9723,10 @@ "glob": "7.1.2", "js-yaml": "3.12.0", "minimatch": "3.0.4", - "resolve": "1.8.1", + "resolve": "1.7.1", "semver": "5.5.0", - "tslib": "1.9.3", - "tsutils": "2.27.1" + "tslib": "1.9.2", + "tsutils": "2.29.0" }, "dependencies": { "chalk": { @@ -9713,12 +9743,12 @@ } }, "tsutils": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", - "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "requires": { - "tslib": "1.9.3" + "tslib": "1.9.2" } }, "tty-browserify": { @@ -9775,9 +9805,9 @@ "dev": true }, "uglify-js": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.2.tgz", - "integrity": "sha512-/kVQDzwiE9Vy7Y63eMkMozF4jIt0C2+xHctF9YpqNWdE/NLOuMurshkpoYGUlAbeYhACPv0HJPIHJul0Ak4/uw==", + "version": "3.3.28", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz", + "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==", "dev": true, "requires": { "commander": "2.15.1", @@ -9800,9 +9830,9 @@ "optional": true }, "uglifyjs-webpack-plugin": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz", - "integrity": "sha512-1VicfKhCYHLS8m1DCApqBhoulnASsEoJ/BvpUpP4zoNAPpKzdH+ghk0olGJMmwX2/jprK2j3hAHdUbczBSy2FA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz", + "integrity": "sha512-hIQJ1yxAPhEA2yW/i7Fr+SXZVMp+VEI3d42RTHBgQd2yhp/1UdBcR3QEWPV5ahBxlqQDMEMTuTEvDHSFINfwSw==", "dev": true, "requires": { "cacache": "10.0.4", @@ -10143,7 +10173,7 @@ "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", "dev": true, "requires": { - "chokidar": "2.0.4", + "chokidar": "2.0.3", "graceful-fs": "4.1.11", "neo-async": "2.5.1" } @@ -10226,7 +10256,7 @@ "@webassemblyjs/ast": "1.4.3", "@webassemblyjs/wasm-edit": "1.4.3", "@webassemblyjs/wasm-parser": "1.4.3", - "acorn": "5.7.1", + "acorn": "5.6.2", "acorn-dynamic-import": "3.0.0", "ajv": "6.4.0", "ajv-keywords": "3.2.0", @@ -10242,7 +10272,7 @@ "node-libs-browser": "2.1.0", "schema-utils": "0.4.5", "tapable": "1.0.0", - "uglifyjs-webpack-plugin": "1.2.7", + "uglifyjs-webpack-plugin": "1.2.5", "watchpack": "1.6.0", "webpack-sources": "1.1.0" } @@ -10306,7 +10336,7 @@ "ansi-html": "0.0.7", "array-includes": "3.0.3", "bonjour": "3.5.0", - "chokidar": "2.0.4", + "chokidar": "2.0.3", "compression": "1.7.2", "connect-history-api-fallback": "1.5.0", "debug": "3.1.0", diff --git a/gae/frontend/package.json b/gae/frontend/package.json index 6d722be..1511ee3 100644 --- a/gae/frontend/package.json +++ b/gae/frontend/package.json @@ -11,25 +11,29 @@ }, "private": true, "dependencies": { - "@angular/animations": "^6.0.3", - "@angular/common": "^6.0.3", - "@angular/compiler": "^6.0.3", - "@angular/core": "^6.0.3", - "@angular/forms": "^6.0.3", - "@angular/http": "^6.0.3", - "@angular/platform-browser": "^6.0.3", - "@angular/platform-browser-dynamic": "^6.0.3", - "@angular/router": "^6.0.3", + "@angular/animations": "^6.0.6", + "@angular/cdk": "^6.2.1", + "@angular/common": "^6.0.6", + "@angular/compiler": "^6.0.6", + "@angular/core": "^6.0.6", + "@angular/flex-layout": "^6.0.0-beta.16", + "@angular/forms": "^6.0.6", + "@angular/http": "^6.0.6", + "@angular/material": "^6.2.1", + "@angular/platform-browser": "^6.0.6", + "@angular/platform-browser-dynamic": "^6.0.6", + "@angular/router": "^6.0.6", "core-js": "^2.5.4", + "moment": "^2.22.2", + "moment-timezone": "^0.5.21", "rxjs": "^6.0.0", "zone.js": "^0.8.26" }, "devDependencies": { - "@angular/compiler-cli": "^6.0.3", "@angular-devkit/build-angular": "~0.6.8", - "typescript": "~2.7.2", "@angular/cli": "~6.0.8", - "@angular/language-service": "^6.0.3", + "@angular/compiler-cli": "^6.0.6", + "@angular/language-service": "^6.0.6", "@types/jasmine": "~2.8.6", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", @@ -43,6 +47,7 @@ "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.3.0", "ts-node": "~5.0.1", - "tslint": "~5.9.1" + "tslint": "^5.9.1", + "typescript": "^2.7.2" } } -- cgit v1.2.3 From b2508e10e9baade87c81bd85889369efa762c394 Mon Sep 17 00:00:00 2001 From: Hsin-Yi Chen Date: Fri, 22 Jun 2018 17:38:50 +0800 Subject: Add report_persistent_url and report_reference_url to models Bug: 79905934 Test: device --lease True Change-Id: Ieeefd3d17317ad5707c44792695dea80d9370628 --- gae/index.yaml | 4 ++++ gae/webapp/src/endpoint/endpoint_base_test.py | 8 ++++++-- gae/webapp/src/endpoint/job_queue_test.py | 4 ++++ gae/webapp/src/endpoint/schedule_info_test.py | 8 ++++++++ gae/webapp/src/proto/model.py | 8 ++++++++ gae/webapp/src/scheduler/schedule_worker.py | 2 ++ 6 files changed, 32 insertions(+), 2 deletions(-) diff --git a/gae/index.yaml b/gae/index.yaml index d66ceb0..cfd8753 100644 --- a/gae/index.yaml +++ b/gae/index.yaml @@ -53,6 +53,8 @@ indexes: - name: required_device_equipment - name: report_bucket - name: report_spreadsheet_id + - name: report_persistent_url + - name: report_reference_url - kind: LabModel ancestor: no @@ -99,3 +101,5 @@ indexes: - name: image_package_repo_base - name: report_bucket - name: report_spreadsheet_id + - name: report_persistent_url + - name: report_reference_url diff --git a/gae/webapp/src/endpoint/endpoint_base_test.py b/gae/webapp/src/endpoint/endpoint_base_test.py index 1265b05..79d6c68 100644 --- a/gae/webapp/src/endpoint/endpoint_base_test.py +++ b/gae/webapp/src/endpoint/endpoint_base_test.py @@ -64,7 +64,9 @@ class EndpointBaseTest(unittest_base.UnitTestBase): "gsi_pab_account_id", "gsi_vendor_version", "test_storage_type", "test_branch", "test_build_target", "test_build_id", "test_pab_account_id", "retry_count", "infra_log_url", - "image_package_repo_base", "report_bucket", "report_spreadsheet_id" + "image_package_repo_base", "report_bucket", + "report_spreadsheet_id", "report_persistent_url", + "report_reference_url" ] job_message = model.JobMessage() for attr in attrs: @@ -86,7 +88,9 @@ class EndpointBaseTest(unittest_base.UnitTestBase): "test_branch", "test_build_target", "test_build_id", "test_pab_account_id", "timestamp", "heartbeat_stamp", "retry_count", "infra_log_url", "parent_schedule", - "image_package_repo_base", "report_bucket", "report_spreadsheet_id" + "image_package_repo_base", "report_bucket", + "report_spreadsheet_id", "report_persistent_url", + "report_reference_url" ] job = model.JobModel() for attr in attrs: diff --git a/gae/webapp/src/endpoint/job_queue_test.py b/gae/webapp/src/endpoint/job_queue_test.py index 750777d..83fb8e9 100644 --- a/gae/webapp/src/endpoint/job_queue_test.py +++ b/gae/webapp/src/endpoint/job_queue_test.py @@ -71,6 +71,10 @@ class JobQueueTest(unittest_base.UnitTestBase): "retry_count": 2, "infra_log_url": self.GetRandomString(), "image_package_repo_base": self.GetRandomString(), + "report_bucket": [self.GetRandomString()], + "report_spreadsheet_id": [self.GetRandomString()], + "report_persistent_url": [self.GetRandomString()], + "report_reference_url": [self.GetRandomString()], } for serial in test_values["serial"]: diff --git a/gae/webapp/src/endpoint/schedule_info_test.py b/gae/webapp/src/endpoint/schedule_info_test.py index b9d1708..61e69ae 100644 --- a/gae/webapp/src/endpoint/schedule_info_test.py +++ b/gae/webapp/src/endpoint/schedule_info_test.py @@ -66,6 +66,10 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): test_build_target=self.GetRandomString(), test_pab_account_id=self.GetRandomString(), image_package_repo_base=self.GetRandomString(), + report_bucket=[self.GetRandomString()], + report_spreadsheet_id=[self.GetRandomString()], + report_persistent_url=[self.GetRandomString()], + report_reference_url=[self.GetRandomString()], )) api = schedule_info.ScheduleInfoApi() response = api.set(container) @@ -107,6 +111,10 @@ class ScheduleInfoTest(unittest_base.UnitTestBase): test_build_target=self.GetRandomString(), test_pab_account_id=self.GetRandomString(), image_package_repo_base=self.GetRandomString(), + report_bucket=[], + report_spreadsheet_id=[], + report_persistent_url=[], + report_reference_url=[], )) api = schedule_info.ScheduleInfoApi() response = api.set(container) diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 7a300f7..506eca8 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -99,6 +99,8 @@ class ScheduleModel(ndb.Model): report_bucket = ndb.StringProperty(repeated=True) report_spreadsheet_id = ndb.StringProperty(repeated=True) + report_persistent_url = ndb.StringProperty(repeated=True) + report_reference_url = ndb.StringProperty(repeated=True) class ScheduleControlInfoMessage(messages.Message): @@ -149,6 +151,8 @@ class ScheduleInfoMessage(messages.Message): report_bucket = messages.StringField(29, repeated=True) report_spreadsheet_id = messages.StringField(30, repeated=True) + report_persistent_url = messages.StringField(32, repeated=True) + report_reference_url = messages.StringField(33, repeated=True) image_package_repo_base = messages.StringField(31) @@ -269,6 +273,8 @@ class JobModel(ndb.Model): report_bucket = ndb.StringProperty(repeated=True) report_spreadsheet_id = ndb.StringProperty(repeated=True) + report_persistent_url = ndb.StringProperty(repeated=True) + report_reference_url = ndb.StringProperty(repeated=True) class JobMessage(messages.Message): @@ -320,6 +326,8 @@ class JobMessage(messages.Message): report_bucket = messages.StringField(33, repeated=True) report_spreadsheet_id = messages.StringField(34, repeated=True) + report_persistent_url = messages.StringField(35, repeated=True) + report_reference_url = messages.StringField(36, repeated=True) class ReturnCodeMessage(messages.Enum): diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 6e1a865..abb919f 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -308,6 +308,8 @@ class ScheduleHandler(webapp2.RequestHandler): new_job.has_radio_img = schedule.has_radio_img new_job.report_bucket = schedule.report_bucket new_job.report_spreadsheet_id = schedule.report_spreadsheet_id + new_job.report_persistent_url = schedule.report_persistent_url + new_job.report_reference_url = schedule.report_reference_url # uses bit 0-1 to indicate version. test_type = GetTestVersionType(schedule.manifest_branch, -- cgit v1.2.3 From f32189535d55e8847f5d881b705be5ceba74aa05 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 6 Aug 2018 14:46:03 +0900 Subject: Update default UI with Angular routing. Test: npm install && ng serve Bug: 74575555 Change-Id: I300194ceb935831625dfba6abcdae212191eed49 --- gae/frontend/src/app/app.component.css | 0 gae/frontend/src/app/app.component.html | 38 ++++++++-------- gae/frontend/src/app/app.component.scss | 0 gae/frontend/src/app/app.component.spec.ts | 27 ----------- gae/frontend/src/app/app.component.ts | 19 +++++++- gae/frontend/src/app/app.module.ts | 52 +++++++++++++++++++++- .../src/app/menu/build/build.component.html | 15 +++++++ .../src/app/menu/build/build.component.scss | 0 gae/frontend/src/app/menu/build/build.component.ts | 25 +++++++++++ .../app/menu/dashboard/dashboard.component.html | 15 +++++++ .../app/menu/dashboard/dashboard.component.scss | 0 .../src/app/menu/dashboard/dashboard.component.ts | 24 ++++++++++ .../src/app/menu/device/device.component.html | 15 +++++++ .../src/app/menu/device/device.component.scss | 0 .../src/app/menu/device/device.component.ts | 25 +++++++++++ gae/frontend/src/app/menu/job/job.component.html | 15 +++++++ gae/frontend/src/app/menu/job/job.component.scss | 0 gae/frontend/src/app/menu/job/job.component.ts | 25 +++++++++++ gae/frontend/src/app/menu/lab/lab.component.html | 15 +++++++ gae/frontend/src/app/menu/lab/lab.component.scss | 0 gae/frontend/src/app/menu/lab/lab.component.ts | 25 +++++++++++ gae/frontend/src/app/menu/menu-items.ts | 23 ++++++++++ .../src/app/menu/schedule/schedule.component.html | 15 +++++++ .../src/app/menu/schedule/schedule.component.scss | 0 .../src/app/menu/schedule/schedule.component.ts | 25 +++++++++++ gae/frontend/src/app/shared/dict.pipe.ts | 29 ++++++++++++ .../src/app/shared/navbar/navbar.component.html | 20 +++++++++ .../src/app/shared/navbar/navbar.component.scss | 10 +++++ .../src/app/shared/navbar/navbar.component.ts | 29 ++++++++++++ gae/frontend/src/app/shared/navbar/navbar.ts | 47 +++++++++++++++++++ gae/frontend/src/index.html | 7 +-- 31 files changed, 487 insertions(+), 53 deletions(-) delete mode 100644 gae/frontend/src/app/app.component.css create mode 100644 gae/frontend/src/app/app.component.scss create mode 100644 gae/frontend/src/app/menu/build/build.component.html create mode 100644 gae/frontend/src/app/menu/build/build.component.scss create mode 100644 gae/frontend/src/app/menu/build/build.component.ts create mode 100644 gae/frontend/src/app/menu/dashboard/dashboard.component.html create mode 100644 gae/frontend/src/app/menu/dashboard/dashboard.component.scss create mode 100644 gae/frontend/src/app/menu/dashboard/dashboard.component.ts create mode 100644 gae/frontend/src/app/menu/device/device.component.html create mode 100644 gae/frontend/src/app/menu/device/device.component.scss create mode 100644 gae/frontend/src/app/menu/device/device.component.ts create mode 100644 gae/frontend/src/app/menu/job/job.component.html create mode 100644 gae/frontend/src/app/menu/job/job.component.scss create mode 100644 gae/frontend/src/app/menu/job/job.component.ts create mode 100644 gae/frontend/src/app/menu/lab/lab.component.html create mode 100644 gae/frontend/src/app/menu/lab/lab.component.scss create mode 100644 gae/frontend/src/app/menu/lab/lab.component.ts create mode 100644 gae/frontend/src/app/menu/menu-items.ts create mode 100644 gae/frontend/src/app/menu/schedule/schedule.component.html create mode 100644 gae/frontend/src/app/menu/schedule/schedule.component.scss create mode 100644 gae/frontend/src/app/menu/schedule/schedule.component.ts create mode 100644 gae/frontend/src/app/shared/dict.pipe.ts create mode 100644 gae/frontend/src/app/shared/navbar/navbar.component.html create mode 100644 gae/frontend/src/app/shared/navbar/navbar.component.scss create mode 100644 gae/frontend/src/app/shared/navbar/navbar.component.ts create mode 100644 gae/frontend/src/app/shared/navbar/navbar.ts diff --git a/gae/frontend/src/app/app.component.css b/gae/frontend/src/app/app.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/gae/frontend/src/app/app.component.html b/gae/frontend/src/app/app.component.html index fa2706a..a4df15a 100644 --- a/gae/frontend/src/app/app.component.html +++ b/gae/frontend/src/app/app.component.html @@ -1,20 +1,20 @@ - -
-

- Welcome to {{ title }}! -

- Angular Logo -
-

Here are some links to help you start:

- + +
+
+ +
+
+ diff --git a/gae/frontend/src/app/app.component.scss b/gae/frontend/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/app.component.spec.ts b/gae/frontend/src/app/app.component.spec.ts index decc558..e69de29 100644 --- a/gae/frontend/src/app/app.component.spec.ts +++ b/gae/frontend/src/app/app.component.spec.ts @@ -1,27 +0,0 @@ -import { TestBed, async } from '@angular/core/testing'; -import { AppComponent } from './app.component'; -describe('AppComponent', () => { - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - AppComponent - ], - }).compileComponents(); - })); - it('should create the app', async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); - it(`should have as title 'app'`, async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app.title).toEqual('app'); - })); - it('should render title in a h1 tag', async(() => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.debugElement.nativeElement; - expect(compiled.querySelector('h1').textContent).toContain('Welcome to frontend!'); - })); -}); diff --git a/gae/frontend/src/app/app.component.ts b/gae/frontend/src/app/app.component.ts index 7b0f672..9e83762 100644 --- a/gae/frontend/src/app/app.component.ts +++ b/gae/frontend/src/app/app.component.ts @@ -1,10 +1,25 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] + styleUrls: ['./app.component.scss'] }) export class AppComponent { - title = 'app'; } diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index f657163..21ef4c4 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -1,14 +1,62 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Angular modules. import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +// User components. import { AppComponent } from './app.component'; +import { BuildComponent } from './menu/build/build.component'; +import { DashboardComponent } from './menu/dashboard/dashboard.component'; +import { DeviceComponent } from './menu/device/device.component'; +import { JobComponent } from './menu/job/job.component'; +import { LabComponent } from './menu/lab/lab.component'; +import { ScheduleComponent } from './menu/schedule/schedule.component'; + +// User modules. +import { NavModule } from './shared/navbar/navbar'; + + +const appRoutes: Routes = [ + { path: 'device', component: DeviceComponent }, + { path: 'build', component: BuildComponent }, + { path: 'job', component: JobComponent }, + { path: 'lab', component: LabComponent }, + { path: 'schedule', component: ScheduleComponent }, + { path: '', component: DashboardComponent }, + { path: '**', redirectTo: '/', pathMatch: 'full' } +]; @NgModule({ declarations: [ - AppComponent + AppComponent, + BuildComponent, + DashboardComponent, + DeviceComponent, + JobComponent, + LabComponent, + ScheduleComponent, ], imports: [ - BrowserModule + BrowserModule, + NavModule, + RouterModule.forRoot( + appRoutes + ), ], providers: [], bootstrap: [AppComponent] diff --git a/gae/frontend/src/app/menu/build/build.component.html b/gae/frontend/src/app/menu/build/build.component.html new file mode 100644 index 0000000..70ff7ff --- /dev/null +++ b/gae/frontend/src/app/menu/build/build.component.html @@ -0,0 +1,15 @@ + +Build page diff --git a/gae/frontend/src/app/menu/build/build.component.scss b/gae/frontend/src/app/menu/build/build.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts new file mode 100644 index 0000000..5f78c51 --- /dev/null +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-build', + templateUrl: './build.component.html', + providers: [], + styleUrls: ['./build.component.scss'], +}) +export class BuildComponent { +} diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.html b/gae/frontend/src/app/menu/dashboard/dashboard.component.html new file mode 100644 index 0000000..0ddc6ef --- /dev/null +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.html @@ -0,0 +1,15 @@ + +VTS Scheduler Dashboard diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.scss b/gae/frontend/src/app/menu/dashboard/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts new file mode 100644 index 0000000..2409b15 --- /dev/null +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class DashboardComponent { +} diff --git a/gae/frontend/src/app/menu/device/device.component.html b/gae/frontend/src/app/menu/device/device.component.html new file mode 100644 index 0000000..38d76ca --- /dev/null +++ b/gae/frontend/src/app/menu/device/device.component.html @@ -0,0 +1,15 @@ + +Device page diff --git a/gae/frontend/src/app/menu/device/device.component.scss b/gae/frontend/src/app/menu/device/device.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts new file mode 100644 index 0000000..76a969d --- /dev/null +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-device', + templateUrl: './device.component.html', + providers: [], + styleUrls: ['./device.component.scss'], +}) +export class DeviceComponent { +} diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html new file mode 100644 index 0000000..623aa7e --- /dev/null +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -0,0 +1,15 @@ + +Job page diff --git a/gae/frontend/src/app/menu/job/job.component.scss b/gae/frontend/src/app/menu/job/job.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts new file mode 100644 index 0000000..ac2e1ae --- /dev/null +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-job', + templateUrl: './job.component.html', + providers: [], + styleUrls: ['./job.component.scss'], +}) +export class JobComponent { +} diff --git a/gae/frontend/src/app/menu/lab/lab.component.html b/gae/frontend/src/app/menu/lab/lab.component.html new file mode 100644 index 0000000..4988794 --- /dev/null +++ b/gae/frontend/src/app/menu/lab/lab.component.html @@ -0,0 +1,15 @@ + +Lab page diff --git a/gae/frontend/src/app/menu/lab/lab.component.scss b/gae/frontend/src/app/menu/lab/lab.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts new file mode 100644 index 0000000..9221dda --- /dev/null +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-lab', + templateUrl: './lab.component.html', + providers: [], + styleUrls: ['./lab.component.scss'], +}) +export class LabComponent { +} diff --git a/gae/frontend/src/app/menu/menu-items.ts b/gae/frontend/src/app/menu/menu-items.ts new file mode 100644 index 0000000..a544366 --- /dev/null +++ b/gae/frontend/src/app/menu/menu-items.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const MENUS = { + ['VTSLab Scheduler']: '/', + ['build']: '/build', + ['device']: '/device', + ['lab']: '/lab', + ['schedule']: '/schedule', + ['job']: '/job', +}; diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html new file mode 100644 index 0000000..8fb9e9a --- /dev/null +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -0,0 +1,15 @@ + +Schedule page diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.scss b/gae/frontend/src/app/menu/schedule/schedule.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts new file mode 100644 index 0000000..bb08b05 --- /dev/null +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-schedule', + templateUrl: './schedule.component.html', + providers: [], + styleUrls: ['./schedule.component.scss'], +}) +export class ScheduleComponent { +} diff --git a/gae/frontend/src/app/shared/dict.pipe.ts b/gae/frontend/src/app/shared/dict.pipe.ts new file mode 100644 index 0000000..44f5933 --- /dev/null +++ b/gae/frontend/src/app/shared/dict.pipe.ts @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({name: 'dict'}) +export class DictPipe implements PipeTransform { + transform(value: Object): any { + const dict = []; + for (const key in value) { + if (value.hasOwnProperty(key)) { + dict.push({key: key, value: value[key]}); + } + } + return dict; + } +} diff --git a/gae/frontend/src/app/shared/navbar/navbar.component.html b/gae/frontend/src/app/shared/navbar/navbar.component.html new file mode 100644 index 0000000..7719d56 --- /dev/null +++ b/gae/frontend/src/app/shared/navbar/navbar.component.html @@ -0,0 +1,20 @@ + + + + {{ menu.key }} + + + diff --git a/gae/frontend/src/app/shared/navbar/navbar.component.scss b/gae/frontend/src/app/shared/navbar/navbar.component.scss new file mode 100644 index 0000000..5b6dc86 --- /dev/null +++ b/gae/frontend/src/app/shared/navbar/navbar.component.scss @@ -0,0 +1,10 @@ +mat-toolbar { + .mat-list-item { + font-family: 'Google Sans', Roboto, sans-serif; + text-transform: capitalize; + } +} + +.mat-list-item { + float: left; +} diff --git a/gae/frontend/src/app/shared/navbar/navbar.component.ts b/gae/frontend/src/app/shared/navbar/navbar.component.ts new file mode 100644 index 0000000..3efe93f --- /dev/null +++ b/gae/frontend/src/app/shared/navbar/navbar.component.ts @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +import { MENUS } from '../../menu/menu-items'; + +@Component({ + selector: 'app-nav-bar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'] +}) +export class NavBarComponent { + get menus() { + return MENUS; + } +} diff --git a/gae/frontend/src/app/shared/navbar/navbar.ts b/gae/frontend/src/app/shared/navbar/navbar.ts new file mode 100644 index 0000000..805dcc5 --- /dev/null +++ b/gae/frontend/src/app/shared/navbar/navbar.ts @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Angular modules. +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +// Angular Material modules. +import { MatButtonModule } from '@angular/material/button'; +import { MatListModule } from '@angular/material/list'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +// User modules. +import { DictPipe } from '../dict.pipe'; +import { NavBarComponent } from './navbar.component'; + +@NgModule({ + declarations: [ + DictPipe, + NavBarComponent, + ], + imports: [ + BrowserModule, + MatButtonModule, + MatToolbarModule, + MatListModule, + RouterModule, + ], + exports: [ + NavBarComponent, + ], + providers: [], +}) +export class NavModule { } diff --git a/gae/frontend/src/index.html b/gae/frontend/src/index.html index 3faefb6..3234be2 100644 --- a/gae/frontend/src/index.html +++ b/gae/frontend/src/index.html @@ -1,14 +1,15 @@ - - Frontend + VTSLab Scheduler + + - + Loading... -- cgit v1.2.3 From 25765557a900b890959f5357acc6406451efbbfa Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 9 Aug 2018 16:35:20 +0900 Subject: Add frontend models representing NDB models. Test: mma Bug: 74575555 --- gae/frontend/src/app/model/build.ts | 24 ++++++++++ gae/frontend/src/app/model/build_wrapper.ts | 21 ++++++++ gae/frontend/src/app/model/device.ts | 23 +++++++++ gae/frontend/src/app/model/device_wrapper.ts | 21 ++++++++ gae/frontend/src/app/model/host.ts | 25 ++++++++++ gae/frontend/src/app/model/host_wrapper.ts | 23 +++++++++ gae/frontend/src/app/model/job.ts | 66 ++++++++++++++++++++++++++ gae/frontend/src/app/model/job_wrapper.ts | 21 ++++++++ gae/frontend/src/app/model/lab.ts | 23 +++++++++ gae/frontend/src/app/model/schedule.ts | 58 ++++++++++++++++++++++ gae/frontend/src/app/model/schedule_wrapper.ts | 21 ++++++++ gae/frontend/src/app/model/tslint.json | 9 ++++ 12 files changed, 335 insertions(+) create mode 100644 gae/frontend/src/app/model/build.ts create mode 100644 gae/frontend/src/app/model/build_wrapper.ts create mode 100644 gae/frontend/src/app/model/device.ts create mode 100644 gae/frontend/src/app/model/device_wrapper.ts create mode 100644 gae/frontend/src/app/model/host.ts create mode 100644 gae/frontend/src/app/model/host_wrapper.ts create mode 100644 gae/frontend/src/app/model/job.ts create mode 100644 gae/frontend/src/app/model/job_wrapper.ts create mode 100644 gae/frontend/src/app/model/lab.ts create mode 100644 gae/frontend/src/app/model/schedule.ts create mode 100644 gae/frontend/src/app/model/schedule_wrapper.ts create mode 100644 gae/frontend/src/app/model/tslint.json diff --git a/gae/frontend/src/app/model/build.ts b/gae/frontend/src/app/model/build.ts new file mode 100644 index 0000000..9e946d1 --- /dev/null +++ b/gae/frontend/src/app/model/build.ts @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class Build { + manifest_branch: string = void 0; + build_id: string = void 0; + build_target: string = void 0; + build_type: string = void 0; + artifact_type: string = void 0; + artifacts: string[] = void 0; + signed: boolean = void 0; +} diff --git a/gae/frontend/src/app/model/build_wrapper.ts b/gae/frontend/src/app/model/build_wrapper.ts new file mode 100644 index 0000000..797d097 --- /dev/null +++ b/gae/frontend/src/app/model/build_wrapper.ts @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Build} from './build'; + +export interface BuildWrapper { + builds: Build[]; + has_next: boolean; +} diff --git a/gae/frontend/src/app/model/device.ts b/gae/frontend/src/app/model/device.ts new file mode 100644 index 0000000..3a6394b --- /dev/null +++ b/gae/frontend/src/app/model/device.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class Device { + serial: string = void 0; + product: string = void 0; + status: number = void 0; + scheduling_status: number = void 0; + hostname: string = void 0; + device_equipment: string[] = void 0; +} diff --git a/gae/frontend/src/app/model/device_wrapper.ts b/gae/frontend/src/app/model/device_wrapper.ts new file mode 100644 index 0000000..af18dce --- /dev/null +++ b/gae/frontend/src/app/model/device_wrapper.ts @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Device} from './device'; + +export interface DeviceWrapper { + devices: Device[]; + has_next: boolean; +} diff --git a/gae/frontend/src/app/model/host.ts b/gae/frontend/src/app/model/host.ts new file mode 100644 index 0000000..3836c30 --- /dev/null +++ b/gae/frontend/src/app/model/host.ts @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class Host { + name: string = void 0; // lab name + owner: string = void 0; + admin: string[] = void 0; + hostname: string = void 0; + ip: string = void 0; + devices: string = void 0; + vtslab_version: string = void 0; + host_equipment: string[] = void 0; +} diff --git a/gae/frontend/src/app/model/host_wrapper.ts b/gae/frontend/src/app/model/host_wrapper.ts new file mode 100644 index 0000000..ae18a04 --- /dev/null +++ b/gae/frontend/src/app/model/host_wrapper.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Host} from './host'; + +export interface HostWrapper { + // Back-end stores each host information as LabModel entity, so it sends + // host information as 'labs'. + labs: Host[]; + has_next: boolean; +} diff --git a/gae/frontend/src/app/model/job.ts b/gae/frontend/src/app/model/job.ts new file mode 100644 index 0000000..69e45b7 --- /dev/null +++ b/gae/frontend/src/app/model/job.ts @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class Job { + test_type: number = void 0; + + hostname: string = void 0; + priority: string = void 0; + test_name: string = void 0; + require_signed_device_build: boolean = void 0; + has_bootloader_img: boolean = void 0; + has_radio_img: boolean = void 0; + device: string = void 0; + serial: string = void 0; + + // device image information + build_storage_type: number = void 0; + manifest_branch: string = void 0; + build_target: string = void 0; + build_id: string = void 0; + pab_account_id: string = void 0; + + shards: number = void 0; + param: string = void 0; + status: number = void 0; + period: number = void 0; + + // GSI information + gsi_storage_type: number = void 0; + gsi_branch: string = void 0; + gsi_build_target: string = void 0; + gsi_build_id: string = void 0; + gsi_pab_account_id: string = void 0; + gsi_vendor_version: string = void 0; + + // test suite information + test_storage_type: number = void 0; + test_branch: string = void 0; + test_build_target: string = void 0; + test_build_id: string = void 0; + test_pab_account_id: string = void 0; + + retry_count: number = void 0; + + infra_log_url: string = void 0; + + image_package_repo_base: string = void 0; + + report_bucket: string = void 0; + report_spreadsheet_id: string = void 0; + + timestamp = void 0; + heartbeat_stamp = void 0; +} diff --git a/gae/frontend/src/app/model/job_wrapper.ts b/gae/frontend/src/app/model/job_wrapper.ts new file mode 100644 index 0000000..5a1f915 --- /dev/null +++ b/gae/frontend/src/app/model/job_wrapper.ts @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Job} from './job'; + +export interface JobWrapper { + jobs: Job[]; + has_next: boolean; +} diff --git a/gae/frontend/src/app/model/lab.ts b/gae/frontend/src/app/model/lab.ts new file mode 100644 index 0000000..0f98360 --- /dev/null +++ b/gae/frontend/src/app/model/lab.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Host} from './host'; + +export class Lab { + name: string = void 0; + owner: string = void 0; + admin: string[] = void 0; + hosts: Host[] = void 0; +} diff --git a/gae/frontend/src/app/model/schedule.ts b/gae/frontend/src/app/model/schedule.ts new file mode 100644 index 0000000..f009e9e --- /dev/null +++ b/gae/frontend/src/app/model/schedule.ts @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class Schedule { + name: string = void 0; + schedule_type: string = void 0; + + // device image information + build_storage_type: number = void 0; + manifest_branch: string = void 0; + build_target: string = void 0; + device_pab_account_id: string = void 0; + require_signed_device_build: boolean = void 0; + has_bootloader_img: boolean = void 0; + has_radio_img: boolean = void 0; + + // GSI information + gsi_storage_type: number = void 0; + gsi_branch: string = void 0; + gsi_build_target: string = void 0; + gsi_pab_account_id: string = void 0; + gsi_vendor_version: string = void 0; + + // test suite information + test_storage_type: number = void 0; + test_branch: string = void 0; + test_build_target: string = void 0; + test_pab_account_id: string = void 0; + + test_name: string = void 0; + period: number = void 0; + schedule: string = void 0; + priority: string = void 0; + device: string[] = void 0; + shards: number = void 0; + param: string[] = void 0; + retry_count: number = void 0; + + required_host_equipment: string[] = void 0; + required_device_equipment: string[] = void 0; + + report_bucket: string[] = void 0; + report_spreadsheet_id: string[] = void 0; + + timestamp = void 0; +} diff --git a/gae/frontend/src/app/model/schedule_wrapper.ts b/gae/frontend/src/app/model/schedule_wrapper.ts new file mode 100644 index 0000000..2dae800 --- /dev/null +++ b/gae/frontend/src/app/model/schedule_wrapper.ts @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Schedule} from './schedule'; + +export interface ScheduleWrapper { + schedules: Schedule[]; + has_next: boolean; +} diff --git a/gae/frontend/src/app/model/tslint.json b/gae/frontend/src/app/model/tslint.json new file mode 100644 index 0000000..eb9bcd8 --- /dev/null +++ b/gae/frontend/src/app/model/tslint.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tslint.json", + "rules": { + "variable-name": [ + true, + "allow-snake-case" + ] + } +} -- cgit v1.2.3 From 3115c01d421ba16e4798a6fe009c77d983c439f8 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Fri, 10 Aug 2018 15:42:09 +0900 Subject: Create the common query methods. These methods are designed to be used among all endpoints. The coming endpoints 'get' and 'count' will use these methods. Test: python testing/e2e_test.py Bug: 112449127 Change-Id: Ibcb96e340fa72dee2923ee56a8f5b89d181c7718 --- gae/webapp/src/endpoint/endpoint_base.py | 157 ++++++++++++++++++++++++++++++- gae/webapp/src/vtslab_status.py | 19 ++++ 2 files changed, 174 insertions(+), 2 deletions(-) diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py index f243af1..c1d1220 100644 --- a/gae/webapp/src/endpoint/endpoint_base.py +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -19,6 +19,8 @@ from protorpc import messages from protorpc import remote from google.appengine.ext import ndb +from webapp.src import vtslab_status as Status + class EndpointBase(remote.Service): """A base class for endpoint implementation.""" @@ -77,16 +79,167 @@ class EndpointBase(remote.Service): attrs = [ x.name for x in value.all_fields() if not assigned_only or ( - value.get_assigned_value(x.name) not in [None, []]) + value.get_assigned_value(x.name) not in [None, []]) ] elif isinstance(value, ndb.Model): attrs = [ x for x in list(value.to_dict()) if not assigned_only or ( - getattr(value, x, None) not in [None, []]) + getattr(value, x, None) not in [None, []]) ] else: raise ValueError("Only protorpc.messages.Message or ndb.Model " "class are supported.") return attrs + + def Count(self, metaclass, filters=None): + """Counts entities from datastore with options. + + Args: + metaclass: a metaclass for ndb model. + filters: a list of tuples. Each tuple consists of three values: + key, method, and value. + + Returns: + a number of entities. + """ + query, _ = self.CreateQueryFilter(metaclass=metaclass, filters=filters) + return query.count() + + def Fetch(self, + metaclass, + size, + offset=0, + filters=None, + sort_key="", + direction="asc"): + """Fetches entities from datastore with options. + + Args: + metaclass: a metaclass for ndb model. + size: an integer, max number of entities to fetch at once. + offset: an integer, number of query results to skip. + filters: a list of filter tuple, a form of (key: string, + method: integer, value: string). + sort_key: a string, key name to sort by. + direction: a string, "asc" for ascending order and "desc" for + descending order. + + Returns: + a list of fetched entities. + a boolean, True if there is next page or False if not. + """ + query, empty_repeated_field = self.CreateQueryFilter( + metaclass=metaclass, filters=filters) + sorted_query = self.SortQuery( + query=query, + metaclass=metaclass, + sort_key=sort_key, + direction=direction) + + if size: + entities, _, more = sorted_query.fetch_page( + page_size=size, offset=offset) + else: + entities = sorted_query.fetch() + more = False + + if empty_repeated_field: + entities = [ + x for x in entities + if all([not getattr(x, attr) for attr in empty_repeated_field]) + ] + + return entities, more + + def CreateQueryFilter(self, metaclass, filters): + """Creates a query with the given filters. + + Args: + metaclass: a metaclass for ndb model. + filters: a list of tuples. Each tuple consists of three values: + key, method, and value. + + Returns: + a filtered query for the given metaclass. + a list of strings that failed to create the query due to its empty + value for the repeated property. + """ + empty_repeated_field = [] + query = metaclass.query() + if not filters: + return query, empty_repeated_field + + for _filter in filters: + property_key = _filter["key"] + method = _filter["method"] + value = _filter["value"] + if type(value) is str or type(value) is unicode: + if isinstance(metaclass._properties[property_key], + ndb.BooleanProperty): + value = value.lower() in ("yes", "true", "1") + elif isinstance(metaclass._properties[property_key], + ndb.IntegerProperty): + value = int(value) + if metaclass._properties[property_key]._repeated: + if value: + value = [value] + if method == Status.FILTER_METHOD[Status.FILTER_Has]: + query = query.filter( + getattr(metaclass, property_key).IN(value)) + else: + logging.warning( + "You cannot compare repeated " + "properties except 'IN(has)' operation.") + else: + logging.debug("Empty repeated list cannot be queried.") + empty_repeated_field.append(value) + else: + if method == Status.FILTER_METHOD[Status.FILTER_EqualTo]: + query = query.filter( + getattr(metaclass, property_key) == value) + elif method == Status.FILTER_METHOD[Status.FILTER_LessThan]: + query = query.filter( + getattr(metaclass, property_key) < value) + elif method == Status.FILTER_METHOD[Status.FILTER_GreaterThan]: + query = query.filter( + getattr(metaclass, property_key) > value) + elif method == Status.FILTER_METHOD[ + Status.FILTER_LessThanOrEqualTo]: + query = query.filter( + getattr(metaclass, property_key) <= value) + elif method == Status.FILTER_METHOD[ + Status.FILTER_GreaterThanOrEqualTo]: + query = query.filter( + getattr(metaclass, property_key) >= value) + elif method == Status.FILTER_METHOD[Status.FILTER_NotEqualTo]: + query = query.filter( + getattr(metaclass, property_key) != value).order( + getattr(metaclass, property_key), metaclass.key) + elif method == Status.FILTER_METHOD[Status.FILTER_Has]: + query = query.filter( + getattr(metaclass, property_key).IN(value)).order( + getattr(metaclass, property_key), metaclass.key) + else: + logging.warning( + "{} is not supported filter method.".format(method)) + return query, empty_repeated_field + + def SortQuery(self, query, metaclass, sort_key, direction): + """Sorts the given query with sort_key and direction. + + Args: + query: a ndb query to sort. + metaclass: a metaclass for ndb model. + sort_key: a string, key name to sort by. + direction: a string, "asc" for ascending order and "desc" for + descending order. + """ + if sort_key: + if direction == "desc": + query = query.order(-getattr(metaclass, sort_key)) + else: + query = query.order(getattr(metaclass, sort_key)) + + return query diff --git a/gae/webapp/src/vtslab_status.py b/gae/webapp/src/vtslab_status.py index 256c32d..c1d1363 100644 --- a/gae/webapp/src/vtslab_status.py +++ b/gae/webapp/src/vtslab_status.py @@ -101,6 +101,25 @@ TEST_TYPE_DICT = { # # of errors in a row to suspend a schedule NUM_ERRORS_FOR_SUSPENSION = 3 +# filter methods +FILTER_EqualTo = "EqualTo" +FILTER_LessThan = "LessThan" +FILTER_GreaterThan = "GreaterThan" +FILTER_LessThanOrEqualTo = "LessThanOrEqualTo" +FILTER_GreaterThanOrEqualTo = "GreaterThanOrEqualTo" +FILTER_NotEqualTo = "NotEqualTo" +FILTER_Has = "Has" + +FILTER_METHOD = { + FILTER_EqualTo: 1, + FILTER_LessThan: 2, + FILTER_GreaterThan: 3, + FILTER_LessThanOrEqualTo: 4, + FILTER_GreaterThanOrEqualTo: 5, + FILTER_NotEqualTo: 6, + FILTER_Has: 7, +} + def GetPriorityValue(priority): """Helper function to sort jobs based on priority. -- cgit v1.2.3 From 90a6936c63160fd2be03c6d375ecf31d44a70026 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 13 Aug 2018 15:58:39 +0900 Subject: Add HTTP backend services. Test: npm install && ng serve Bug: 74575555 --- gae/frontend/src/app/app.module.ts | 2 + gae/frontend/src/app/menu/build/build.component.ts | 7 +++- gae/frontend/src/app/menu/build/build.service.ts | 48 +++++++++++++++++++++ .../src/app/menu/device/device.component.ts | 7 +++- gae/frontend/src/app/menu/device/device.service.ts | 48 +++++++++++++++++++++ gae/frontend/src/app/menu/job/job.component.ts | 7 +++- gae/frontend/src/app/menu/job/job.service.ts | 49 ++++++++++++++++++++++ gae/frontend/src/app/menu/lab/lab.component.ts | 7 +++- gae/frontend/src/app/menu/lab/lab.service.ts | 49 ++++++++++++++++++++++ .../src/app/menu/schedule/schedule.component.ts | 7 +++- .../src/app/menu/schedule/schedule.service.ts | 49 ++++++++++++++++++++++ gae/frontend/src/app/shared/servicebase.ts | 44 +++++++++++++++++++ 12 files changed, 319 insertions(+), 5 deletions(-) create mode 100644 gae/frontend/src/app/menu/build/build.service.ts create mode 100644 gae/frontend/src/app/menu/device/device.service.ts create mode 100644 gae/frontend/src/app/menu/job/job.service.ts create mode 100644 gae/frontend/src/app/menu/lab/lab.service.ts create mode 100644 gae/frontend/src/app/menu/schedule/schedule.service.ts create mode 100644 gae/frontend/src/app/shared/servicebase.ts diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index 21ef4c4..6222c8d 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -15,6 +15,7 @@ */ // Angular modules. import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; @@ -53,6 +54,7 @@ const appRoutes: Routes = [ ], imports: [ BrowserModule, + HttpClientModule, NavModule, RouterModule.forRoot( appRoutes diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index 5f78c51..2b409c1 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -15,11 +15,16 @@ */ import { Component } from '@angular/core'; +import { BuildService } from './build.service'; + + @Component({ selector: 'app-build', templateUrl: './build.component.html', - providers: [], + providers: [ BuildService ], styleUrls: ['./build.component.scss'], }) export class BuildComponent { + constructor(private buildService: BuildService) { + } } diff --git a/gae/frontend/src/app/menu/build/build.service.ts b/gae/frontend/src/app/menu/build/build.service.ts new file mode 100644 index 0000000..d3e4c6f --- /dev/null +++ b/gae/frontend/src/app/menu/build/build.service.ts @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { catchError } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { BuildWrapper } from '../../model/build_wrapper'; +import { environment } from '../../../environments/environment'; +import { ServiceBase } from '../../shared/servicebase'; + + +@Injectable() +export class BuildService extends ServiceBase { + constructor(public httpClient: HttpClient) { + super(httpClient); + this.url = environment['baseURL'] + '/build_info/v1/'; + } + + getBuilds(size: number, + offset: number, + filterInfo: string, + sort: string, + direction: string): Observable> { + const url = this.url + 'get'; + return this.httpClient.get(url, {observe: 'response', params: new HttpParams() + .append('size', String(size)) + .append('offset', String(offset)) + .append('filter', filterInfo) + .append('sort', sort) + .append('direction', direction)}) + .pipe(catchError(this.handleError)); + } +} diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index 76a969d..340da0d 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -15,11 +15,16 @@ */ import { Component } from '@angular/core'; +import { DeviceService } from './device.service'; + + @Component({ selector: 'app-device', templateUrl: './device.component.html', - providers: [], + providers: [ DeviceService ], styleUrls: ['./device.component.scss'], }) export class DeviceComponent { + constructor(private deviceService: DeviceService) { + } } diff --git a/gae/frontend/src/app/menu/device/device.service.ts b/gae/frontend/src/app/menu/device/device.service.ts new file mode 100644 index 0000000..2223bd4 --- /dev/null +++ b/gae/frontend/src/app/menu/device/device.service.ts @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { catchError } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { environment } from '../../../environments/environment'; +import { DeviceWrapper } from '../../model/device_wrapper'; +import { ServiceBase } from '../../shared/servicebase'; + + +@Injectable() +export class DeviceService extends ServiceBase { + constructor(public httpClient: HttpClient) { + super(httpClient); + this.url = environment['baseURL'] + '/host_info/v1/'; + } + + getDevices(size: number, + offset: number, + filterInfo: string, + sort: string, + direction: string): Observable> { + const url = this.url + 'get'; + return this.httpClient.get(url, {observe: 'response', params: new HttpParams() + .append('size', String(size)) + .append('offset', String(offset)) + .append('filter', filterInfo) + .append('sort', sort) + .append('direction', direction)}) + .pipe(catchError(this.handleError)); + } +} diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index ac2e1ae..a5f2483 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -15,11 +15,16 @@ */ import { Component } from '@angular/core'; +import { JobService } from './job.service'; + + @Component({ selector: 'app-job', templateUrl: './job.component.html', - providers: [], + providers: [ JobService ], styleUrls: ['./job.component.scss'], }) export class JobComponent { + constructor(private jobService: JobService) { + } } diff --git a/gae/frontend/src/app/menu/job/job.service.ts b/gae/frontend/src/app/menu/job/job.service.ts new file mode 100644 index 0000000..3a99d72 --- /dev/null +++ b/gae/frontend/src/app/menu/job/job.service.ts @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { catchError } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { environment } from '../../../environments/environment'; +import { JobWrapper } from '../../model/job_wrapper'; +import { ServiceBase } from '../../shared/servicebase'; + + +@Injectable() +export class JobService extends ServiceBase { + // url: string; + constructor(public httpClient: HttpClient) { + super(httpClient); + this.url = environment['baseURL'] + '/job_queue/v1/'; + } + + getJobs(size: number, + offset: number, + filterInfo: string, + sort: string, + direction: string): Observable> { + const url = this.url + 'get'; + return this.httpClient.get(url, {observe: 'response', params: new HttpParams() + .append('size', String(size)) + .append('offset', String(offset)) + .append('filter', filterInfo) + .append('sort', sort) + .append('direction', direction)}) + .pipe(catchError(this.handleError)); + } +} diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts index 9221dda..6ce4609 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.ts +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -15,11 +15,16 @@ */ import { Component } from '@angular/core'; +import { LabService } from './lab.service'; + + @Component({ selector: 'app-lab', templateUrl: './lab.component.html', - providers: [], + providers: [ LabService ], styleUrls: ['./lab.component.scss'], }) export class LabComponent { + constructor(private labService: LabService) { + } } diff --git a/gae/frontend/src/app/menu/lab/lab.service.ts b/gae/frontend/src/app/menu/lab/lab.service.ts new file mode 100644 index 0000000..0fda86d --- /dev/null +++ b/gae/frontend/src/app/menu/lab/lab.service.ts @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { catchError } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { environment } from '../../../environments/environment'; +import { HostWrapper } from '../../model/host_wrapper'; +import { ServiceBase } from '../../shared/servicebase'; + + +@Injectable() +export class LabService extends ServiceBase { + // url: string; + constructor(public httpClient: HttpClient) { + super(httpClient); + this.url = environment['baseURL'] + '/lab_info/v1/'; + } + + getLabs(size: number, + offset: number, + filterInfo: string, + sort: string, + direction: string): Observable> { + const url = this.url + 'get'; + return this.httpClient.get(url, {observe: 'response', params: new HttpParams() + .append('size', String(size)) + .append('offset', String(offset)) + .append('filter', filterInfo) + .append('sort', sort) + .append('direction', direction)}) + .pipe(catchError(this.handleError)); + } +} diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index bb08b05..69c09ac 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -15,11 +15,16 @@ */ import { Component } from '@angular/core'; +import { ScheduleService } from './schedule.service'; + + @Component({ selector: 'app-schedule', templateUrl: './schedule.component.html', - providers: [], + providers: [ ScheduleService ], styleUrls: ['./schedule.component.scss'], }) export class ScheduleComponent { + constructor(private scheduleService: ScheduleService) { + } } diff --git a/gae/frontend/src/app/menu/schedule/schedule.service.ts b/gae/frontend/src/app/menu/schedule/schedule.service.ts new file mode 100644 index 0000000..cda44f9 --- /dev/null +++ b/gae/frontend/src/app/menu/schedule/schedule.service.ts @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { catchError } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { environment } from '../../../environments/environment'; +import { ScheduleWrapper } from '../../model/schedule_wrapper'; +import { ServiceBase } from '../../shared/servicebase'; + + +@Injectable() +export class ScheduleService extends ServiceBase { + // url: string; + constructor(public httpClient: HttpClient) { + super(httpClient); + this.url = environment['baseURL'] + '/schedule_info/v1/'; + } + + getSchedules(size: number, + offset: number, + filterInfo: string, + sort: string, + direction: string): Observable> { + const url = this.url + 'get'; + return this.httpClient.get(url, {observe: 'response', params: new HttpParams() + .append('size', String(size)) + .append('offset', String(offset)) + .append('filter', filterInfo) + .append('sort', sort) + .append('direction', direction)}) + .pipe(catchError(this.handleError)); + } +} diff --git a/gae/frontend/src/app/shared/servicebase.ts b/gae/frontend/src/app/shared/servicebase.ts new file mode 100644 index 0000000..f7b8c8f --- /dev/null +++ b/gae/frontend/src/app/shared/servicebase.ts @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HttpClient, HttpErrorResponse, HttpParams, HttpResponse } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; + +export class ServiceBase { + url: string; + protected constructor(public httpClient: HttpClient) { + } + protected handleError(error: HttpErrorResponse) { + if (error.error instanceof ErrorEvent) { + // A client-side or network error occurred. Handle it accordingly. + console.error('An error occurred:', error.error.message); + } else { + // The backend returned an unsuccessful response code. + // The response body may contain clues as to what went wrong, + console.error( + `Backend returned code ${error.status}, ` + + `body was: ${error.error}`); + } + // return an observable with a user-facing error message + return throwError( + 'Something bad happened; please try again later.'); + } + public getCount(filterInfo: string): Observable> { + const url = this.url + 'count'; + return this.httpClient.get(url, {observe: 'response', params: new HttpParams() + .append('filter', filterInfo) + }); + } +} -- cgit v1.2.3 From 6db3a493ca5f27f209ed66538229b3ae84529aba Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Fri, 10 Aug 2018 14:30:19 +0900 Subject: Add /get and /count interface on all endpoints. Test: mma Bug: 74575555 --- gae/webapp/src/endpoint/build_info.py | 48 +++++++++++++++++++++++ gae/webapp/src/endpoint/endpoint_base.py | 31 ++++++++++++++- gae/webapp/src/endpoint/host_info.py | 49 +++++++++++++++++++++++ gae/webapp/src/endpoint/job_queue.py | 67 ++++++++++++++++++++++++++++++-- gae/webapp/src/endpoint/lab_info.py | 54 ++++++++++++++++++++++--- gae/webapp/src/endpoint/schedule_info.py | 57 +++++++++++++++++++++++++-- gae/webapp/src/proto/model.py | 61 +++++++++++++++++++++++++++++ 7 files changed, 354 insertions(+), 13 deletions(-) diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index 2ac6a63..e00bbd1 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -68,3 +68,51 @@ class BuildInfoApi(endpoint_base.EndpointBase): return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) + + @endpoints.method( + endpoint_base.GET_REQUEST_RESOURCE, + model.BuildResponseMessage, + path="get", + http_method="GET", + name="get") + def get(self, request): + """Gets the builds from datastore.""" + size = request.size if request.size else endpoint_base.MAX_QUERY_SIZE + offset = request.offset if request.offset else 0 + + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.BuildModel) + + builds, more = self.Fetch( + metaclass=model.BuildModel, + size=size, + filters=filters, + offset=offset, + sort_key=request.sort, + direction=request.direction, + ) + + return_list = [] + for build in builds: + _build = {} + assigned_attributes = self.GetCommonAttributes( + resource=build, reference=model.BuildInfoMessage) + for attr in assigned_attributes: + _build[attr] = getattr(build, attr, None) + return_list.append(_build) + + return model.BuildResponseMessage(builds=return_list, has_next=more) + + @endpoints.method( + endpoint_base.COUNT_REQUEST_RESOURCE, + model.CountResponseMessage, + path="count", + http_method="GET", + name="count") + def count(self, request): + """Gets total number of BuildModel entities stored in datastore.""" + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.BuildModel) + count = self.Count(metaclass=model.BuildModel, filters=filters) + + return model.CountResponseMessage(count=count) diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py index c1d1220..c94346b 100644 --- a/gae/webapp/src/endpoint/endpoint_base.py +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -12,14 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import inspect +import logging +import json +import endpoints from protorpc import messages from protorpc import remote from google.appengine.ext import ndb from webapp.src import vtslab_status as Status +from webapp.src.proto import model + +MAX_QUERY_SIZE = 1000 + +COUNT_REQUEST_RESOURCE = endpoints.ResourceContainer(model.CountRequestMessage) +GET_REQUEST_RESOURCE = endpoints.ResourceContainer(model.GetRequestMessage) class EndpointBase(remote.Service): @@ -243,3 +251,24 @@ class EndpointBase(remote.Service): query = query.order(getattr(metaclass, sort_key)) return query + + def CreateFilterList(self, filter_string, metaclass): + """Creates a list of filters. + + Args: + filter_string: a string, stringified JSON which contains 'key', + 'method', 'value' to build filter information. + metaclass: a metaclass for ndb model. + + Returns: + a list of tuples where each tuple consists of three values: + key, method, and value. + """ + model_properties = self.GetAttributes(metaclass) + filters = [] + if filter_string: + filters = json.loads(filter_string) + for _filter in filters: + if _filter["key"] not in model_properties: + filters.remove(_filter) + return filters diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index 52a4db0..8dd01b6 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -102,3 +102,52 @@ class HostInfoApi(endpoint_base.EndpointBase): return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) + + @endpoints.method( + endpoint_base.GET_REQUEST_RESOURCE, + model.DeviceResponseMessage, + path="get", + http_method="GET", + name="get") + def get(self, request): + """Gets the devices from datastore.""" + size = request.size if request.size else endpoint_base.MAX_QUERY_SIZE + offset = request.offset if request.offset else 0 + + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.DeviceModel) + + devices, more = self.Fetch( + metaclass=model.DeviceModel, + size=size, + filters=filters, + offset=offset, + sort_key=request.sort, + direction=request.direction, + ) + + return_list = [] + for device in devices: + _device = {} + assigned_attributes = self.GetCommonAttributes( + resource=device, reference=model.DeviceInfoMessage) + for attr in assigned_attributes: + _device[attr] = getattr(device, attr, None) + return_list.append(_device) + + return model.DeviceResponseMessage(devices=return_list, has_next=more) + + @endpoints.method( + endpoint_base.COUNT_REQUEST_RESOURCE, + model.CountResponseMessage, + path="count", + http_method="GET", + name="count") + def count(self, request): + """Gets total number of DeviceModel entities stored in datastore.""" + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.DeviceModel) + + count = self.Count(metaclass=model.DeviceModel, filters=filters) + + return model.CountResponseMessage(count=count) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 5806345..1023f84 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -43,6 +43,16 @@ class JobQueueApi(endpoint_base.EndpointBase): http_method='POST', name='get') def get(self, request): + # TODO(jongmok): This will be deprecated and replaced with /lease. + return self.lease(request) + + @endpoints.method( + JOB_QUEUE_RESOURCE, + model.JobLeaseResponse, + path='get', + http_method='POST', + name='get') + def lease(self, request): """Gets the job(s) based on the condition specified in `request`.""" job_query = model.JobModel.query( model.JobModel.hostname == request.hostname, @@ -133,8 +143,10 @@ class JobQueueApi(endpoint_base.EndpointBase): device.scheduling_status = ( Status.DEVICE_SCHEDULING_STATUS_DICT["free"]) devices_to_put.append(device) - elif (request.status in [Status.JOB_STATUS_DICT["infra-err"], - Status.JOB_STATUS_DICT["bootup-err"]]): + elif (request.status in [ + Status.JOB_STATUS_DICT["infra-err"], + Status.JOB_STATUS_DICT["bootup-err"] + ]): job.status = request.status email_util.send_job_notification(job) for device in devices: @@ -169,7 +181,56 @@ class JobQueueApi(endpoint_base.EndpointBase): job.put() model_util.UpdateParentSchedule(job, request.status) return model.JobLeaseResponse( - return_code=model.ReturnCodeMessage.SUCCESS, jobs=[job_message]) + return_code=model.ReturnCodeMessage.SUCCESS, + jobs=[job_message]) return model.JobLeaseResponse( return_code=model.ReturnCodeMessage.FAIL, jobs=[]) + + @endpoints.method( + endpoint_base.GET_REQUEST_RESOURCE, + model.JobResponseMessage, + path="get", + http_method="GET", + name="get") + def get(self, request): + """Gets the jobs from datastore.""" + size = request.size if request.size else self.MAX_QUERY_SIZE + offset = request.offset if request.offset else 0 + + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.JobModel) + + jobs, more = self.Fetch( + metaclass=model.JobModel, + size=size, + filters=filters, + offset=offset, + sort_key=request.sort, + direction=request.direction, + ) + + return_list = [] + for job in jobs: + common_properties = self.GetCommonAttributes( + resource=job, reference=model.JobMessage) + _job = {} + for _property in common_properties: + _job[_property] = getattr(job, _property) + return_list.append(_job) + + return model.JobResponseMessage(jobs=return_list, has_next=more) + + @endpoints.method( + endpoint_base.COUNT_REQUEST_RESOURCE, + model.CountResponseMessage, + path="count", + http_method="GET", + name="count") + def count(self, request): + """Gets total number of JobModel entities stored in datastore.""" + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.JobModel) + count = self.Count(metaclass=model.JobModel, filters=filters) + + return model.CountResponseMessage(count=count) diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index a68e04b..5c90554 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Lab Info APIs implemented using Google Cloud Endpoints.""" import datetime @@ -88,16 +87,16 @@ class LabInfoApi(endpoint_base.EndpointBase): if devices: device = devices[0] if (device.hostname != host.hostname) and ( - device.status != - Status.DEVICE_STATUS_DICT["no-response"]): + device.status != + Status.DEVICE_STATUS_DICT["no-response"]): logging.error( "{} is alive in another host.".format( config_device.serial)) # TODO: send an alert to lab.admin continue if device.hostname == host.hostname and set( - device.device_equipment) == set( - config_device.device_equipment): + device.device_equipment) == set( + config_device.device_equipment): # no need to update. continue else: @@ -152,3 +151,48 @@ class LabInfoApi(endpoint_base.EndpointBase): return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) + + @endpoints.method( + endpoint_base.GET_REQUEST_RESOURCE, + model.LabResponseMessage, + path="get", + http_method="GET", + name="get") + def get(self, request): + """Gets the labs from datastore.""" + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.LabModel) + + labs, more = self.Fetch( + metaclass=model.LabModel, + size=0, + filters=filters, + sort_key=request.sort, + direction=request.direction, + ) + + return_list = [] + for lab in labs: + _lab = {} + assigned_attributes = self.GetCommonAttributes( + resource=lab, reference=model.LabMessage) + for attr in assigned_attributes: + _lab[attr] = getattr(lab, attr, None) + return_list.append(_lab) + + return model.LabResponseMessage(labs=return_list, has_next=more) + + @endpoints.method( + endpoint_base.COUNT_REQUEST_RESOURCE, + model.CountResponseMessage, + path="count", + http_method="GET", + name="count") + def count(self, request): + """Gets total number of BuildModel entities stored in datastore.""" + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.LabModel) + + count = self.Count(metaclass=model.LabModel, filters=filters) + + return model.CountResponseMessage(count=count) diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index 37805bd..be8de99 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -60,9 +60,7 @@ class ScheduleInfoApi(endpoint_base.EndpointBase): "children_jobs", "error_count", "suspended" ] # list of protorpc message fields. - duplicate_checklist = [ - x for x in exist_on_both if x not in exclusions - ] + duplicate_checklist = [x for x in exist_on_both if x not in exclusions] empty_list_field = [] query = model.ScheduleModel.query() for attr_name in duplicate_checklist: @@ -97,12 +95,63 @@ class ScheduleInfoApi(endpoint_base.EndpointBase): schedule.schedule_type = "test" schedule.error_count = 0 schedule.suspended = False - schedule.priority_value = Status.GetPriorityValue(schedule.priority) + schedule.priority_value = Status.GetPriorityValue( + schedule.priority) schedule.put() return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) + @endpoints.method( + endpoint_base.GET_REQUEST_RESOURCE, + model.ScheduleResponseMessage, + path="get", + http_method="GET", + name="get") + def get(self, request): + """Gets the schedules from datastore.""" + size = request.size if request.size else self.MAX_QUERY_SIZE + offset = request.offset if request.offset else 0 + + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.ScheduleModel) + + schedules, more = self.Fetch( + metaclass=model.ScheduleModel, + size=size, + filters=filters, + offset=offset, + sort_key=request.sort, + direction=request.direction, + ) + + return_list = [] + for schedule in schedules: + _schedule = {} + assigned_attributes = self.GetCommonAttributes( + resource=schedule, reference=model.ScheduleInfoMessage) + for attr in assigned_attributes: + _schedule[attr] = getattr(schedule, attr, None) + return_list.append(_schedule) + + return model.ScheduleResponseMessage( + schedules=return_list, has_next=more) + + @endpoints.method( + endpoint_base.COUNT_REQUEST_RESOURCE, + model.CountResponseMessage, + path="count", + http_method="GET", + name="count") + def count(self, request): + """Gets total number of ScheduleModel entities stored in datastore.""" + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=model.ScheduleModel) + + count = self.Count(metaclass=model.ScheduleModel, filters=filters) + + return model.CountResponseMessage(count=count) + @endpoints.api(name="green_schedule_info", version="v1") class GreenScheduleInfoApi(endpoint_base.EndpointBase): diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 506eca8..a354105 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -196,6 +196,18 @@ class LabInfoMessage(messages.Message): host = messages.MessageField(LabHostInfoMessage, 3, repeated=True) +class LabMessage(messages.Message): + """A model for representing a LabModel entity.""" + name = messages.StringField(1) + owner = messages.StringField(2) + admin = messages.StringField(3, repeated=True) + hostname = messages.StringField(4) + ip = messages.StringField(5) + devices = messages.StringField(6) + vtslab_version = messages.StringField(7) + host_equipment = messages.StringField(8, repeated=True) + + class DeviceModel(ndb.Model): """A model for representing an individual device entry.""" hostname = ndb.StringProperty() @@ -358,3 +370,52 @@ class KeyValueModel(ndb.Model): string_value = ndb.StringProperty() integer_value = ndb.IntegerProperty() boolean_value = ndb.BooleanProperty() + + +class GetRequestMessage(messages.Message): + """A message to request entities through /get endpoints.""" + size = messages.IntegerField(1) + offset = messages.IntegerField(2) + filter = messages.StringField(3) + sort = messages.StringField(4) + direction = messages.StringField(5) + + +class BuildResponseMessage(messages.Message): + """A message containing build entities to respond to /get endpoints.""" + builds = messages.MessageField(BuildInfoMessage, 1, repeated=True) + has_next = messages.BooleanField(2) + + +class DeviceResponseMessage(messages.Message): + """A message containing device entities to respond to /get endpoints.""" + devices = messages.MessageField(DeviceInfoMessage, 1, repeated=True) + has_next = messages.BooleanField(2) + + +class JobResponseMessage(messages.Message): + """A message containing job entities to respond to /get endpoints.""" + jobs = messages.MessageField(JobMessage, 1, repeated=True) + has_next = messages.BooleanField(2) + + +class LabResponseMessage(messages.Message): + """A message containing lab entities to respond to /get endpoints.""" + labs = messages.MessageField(LabMessage, 1, repeated=True) + has_next = messages.BooleanField(2) + + +class ScheduleResponseMessage(messages.Message): + """A message containing schedule entities to respond to /get endpoints.""" + schedules = messages.MessageField(ScheduleInfoMessage, 1, repeated=True) + has_next = messages.BooleanField(2) + + +class CountRequestMessage(messages.Message): + """A message to request a count of entities through /count endpoints.""" + filter = messages.StringField(1) + + +class CountResponseMessage(messages.Message): + """A message of a count of entities to respond to /count endpoints.""" + count = messages.IntegerField(1) -- cgit v1.2.3 From b4dc2101c54dfe2ef43aa136f1ceb41f723bb7be Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 8 Aug 2018 14:43:54 +0900 Subject: Add tables and essential components for each page. Test: npm install && ng serve Bug: 74575555 Change-Id: I841ea38c7b0080fac0cc974054902b831addbfa6 --- gae/frontend/src/app/app.module.ts | 10 ++ .../src/app/menu/build/build.component.html | 56 +++++++++- gae/frontend/src/app/menu/build/build.component.ts | 23 +++- .../src/app/menu/device/device.component.html | 56 +++++++++- .../src/app/menu/device/device.component.ts | 24 +++- gae/frontend/src/app/menu/job/job.component.html | 122 ++++++++++++++++++++- gae/frontend/src/app/menu/job/job.component.ts | 35 +++++- gae/frontend/src/app/menu/lab/lab.component.html | 91 ++++++++++++++- gae/frontend/src/app/menu/lab/lab.component.ts | 27 ++++- gae/frontend/src/app/menu/menu_base.ts | 10 ++ .../src/app/menu/schedule/schedule.component.html | 80 +++++++++++++- .../src/app/menu/schedule/schedule.component.ts | 28 ++++- 12 files changed, 552 insertions(+), 10 deletions(-) create mode 100644 gae/frontend/src/app/menu/menu_base.ts diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index 6222c8d..2e26033 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -14,11 +14,17 @@ * limitations under the License. */ // Angular modules. +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +// Angular Material modules +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatTableModule } from '@angular/material/table'; +import { MatTabsModule } from '@angular/material/tabs'; + // User components. import { AppComponent } from './app.component'; import { BuildComponent } from './menu/build/build.component'; @@ -53,8 +59,12 @@ const appRoutes: Routes = [ ScheduleComponent, ], imports: [ + BrowserAnimationsModule, BrowserModule, HttpClientModule, + MatPaginatorModule, + MatTableModule, + MatTabsModule, NavModule, RouterModule.forRoot( appRoutes diff --git a/gae/frontend/src/app/menu/build/build.component.html b/gae/frontend/src/app/menu/build/build.component.html index 70ff7ff..fc0013a 100644 --- a/gae/frontend/src/app/menu/build/build.component.html +++ b/gae/frontend/src/app/menu/build/build.component.html @@ -12,4 +12,58 @@ See the License for the specific language governing permissions and limitations under the License. --> -Build page +
+ + + + No. + {{i+1+pageSize*pageIndex}} + + + + + Artifact Type + {{build.artifact_type}} + + + + + Manifest Branch + {{build.manifest_branch}} + + + + + Build ID + {{build.build_id}} + + + + + Build Target + {{build.build_target}} + + + + + Build Type + {{build.build_type}} + + + + + Signed + {{build.signed}} + + + + + + + + +
diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index 2b409c1..571ce35 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -14,8 +14,11 @@ * limitations under the License. */ import { Component } from '@angular/core'; +import { MatTableDataSource, PageEvent } from '@angular/material'; +import { Build } from '../../model/build'; import { BuildService } from './build.service'; +import { MenuBaseClass } from '../menu_base'; @Component({ @@ -24,7 +27,25 @@ import { BuildService } from './build.service'; providers: [ BuildService ], styleUrls: ['./build.component.scss'], }) -export class BuildComponent { +export class BuildComponent extends MenuBaseClass { + columnTitles = [ + '_index', + 'artifact_type', + 'build_id', + 'build_target', + 'build_type', + 'manifest_branch', + 'signed']; + dataSource = new MatTableDataSource(); + pageEvent: PageEvent; + constructor(private buildService: BuildService) { + super(); + } + + onPageEvent(event: PageEvent) { + this.pageSize = event.pageSize; + this.pageIndex = event.pageIndex; + return event; } } diff --git a/gae/frontend/src/app/menu/device/device.component.html b/gae/frontend/src/app/menu/device/device.component.html index 38d76ca..f70447c 100644 --- a/gae/frontend/src/app/menu/device/device.component.html +++ b/gae/frontend/src/app/menu/device/device.component.html @@ -12,4 +12,58 @@ See the License for the specific language governing permissions and limitations under the License. --> -Device page +
+ + + + No. + {{i+1+pageSize*pageIndex}} + + + + + Host Name + {{device.hostname}} + + + + + Product + {{device.product}} + + + + + Serial + {{device.serial}} + + + + + Status + {{device.status}} + + + + + Scheduling Status + {{device.scheduling_status}} + + + + + Equipment + {{device.device_equipment ? device.device_equipment.join(", ") : "None"}} + + + + +
+ + + +
diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index 340da0d..542674c 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -14,8 +14,11 @@ * limitations under the License. */ import { Component } from '@angular/core'; +import { MatTableDataSource, PageEvent } from '@angular/material'; +import { Device } from '../../model/device'; import { DeviceService } from './device.service'; +import { MenuBaseClass } from '../menu_base'; @Component({ @@ -24,7 +27,26 @@ import { DeviceService } from './device.service'; providers: [ DeviceService ], styleUrls: ['./device.component.scss'], }) -export class DeviceComponent { +export class DeviceComponent extends MenuBaseClass { + columnTitles = [ + '_index', + 'device_equipment', + 'hostname', + 'product', + 'scheduling_status', + 'serial', + 'status', + ]; + dataSource = new MatTableDataSource(); + pageEvent: PageEvent; + constructor(private deviceService: DeviceService) { + super(); + } + + onPageEvent(event: PageEvent) { + this.pageSize = event.pageSize; + this.pageIndex = event.pageIndex; + return event; } } diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 623aa7e..d4b2c2e 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -12,4 +12,124 @@ See the License for the specific language governing permissions and limitations under the License. --> -Job page +
+ + + + No. + {{i+1+pageSize*pageIndex}} + + + + + Test Type + {{job.test_type}} + + + + + Test Name + {{job.test_name}} + + + + + Hostname + {{job.hostname}} + + + + + Device + {{job.device}} + + + + + Serial + {{job.serial.join('\n')}} + + + + + Device Branch + {{job.manifest_branch}} + + + + + Device Build Target + {{job.build_target}} + + + + + Device Build ID + {{job.build_id}} + + + + + GSI Branch + {{job.gsi_branch}} + + + + + GSI Build Target + {{job.gsi_build_target}} + + + + + GSI Build ID + {{job.gsi_build_id}} + + + + + Test Branch + {{job.test_branch}} + + + + + Test Build Target + {{job.test_build_target}} + + + + + Test Build ID + {{job.test_build_id}} + + + + + Status + {{job.status}} + + + + + Timestamp + {{job.timestamp}} + + + + + Heartbeat + {{job.heartbeat_stamp}} + + + + +
+ + + +
diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index a5f2483..cc50596 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -14,7 +14,10 @@ * limitations under the License. */ import { Component } from '@angular/core'; +import { MatTableDataSource, PageEvent } from '@angular/material'; +import { MenuBaseClass } from '../menu_base'; +import { Job } from '../../model/job'; import { JobService } from './job.service'; @@ -24,7 +27,37 @@ import { JobService } from './job.service'; providers: [ JobService ], styleUrls: ['./job.component.scss'], }) -export class JobComponent { +export class JobComponent extends MenuBaseClass { + columnTitles = [ + '_index', + 'build_id', + 'build_target', + 'device', + 'gsi_branch', + 'gsi_build_id', + 'gsi_build_target', + 'heartbeat_stamp', + 'hostname', + 'manifest_branch', + 'serial', + 'status', + 'test_branch', + 'test_build_id', + 'test_build_target', + 'test_name', + 'test_type', + 'timestamp', + ]; + dataSource = new MatTableDataSource(); + pageEvent: PageEvent; + constructor(private jobService: JobService) { + super(); + } + + onPageEvent(event: PageEvent) { + this.pageSize = event.pageSize; + this.pageIndex = event.pageIndex; + return event; } } diff --git a/gae/frontend/src/app/menu/lab/lab.component.html b/gae/frontend/src/app/menu/lab/lab.component.html index 4988794..a0701f9 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.html +++ b/gae/frontend/src/app/menu/lab/lab.component.html @@ -12,4 +12,93 @@ See the License for the specific language governing permissions and limitations under the License. --> -Lab page + + +
+ + + + No. + {{i+1+pageSize*labPageIndex}} + + + + + Name + {{lab.name}} + + + + + Owner + {{lab.owner}} + + + + + Admin + {{lab.admin ? lab.admin.join(", ") : "None"}} + + + + + # of Host + {{ lab.hosts.length }} + + + + + + + +
+
+ +
+ + + + No. + {{i+1+pageSize*pageIndex}} + + + + + Lab + {{host.name}} + + + + + Hostname + {{host.hostname}} + + + + + IP + {{host.ip}} + + + + + Version + {{host.vtslab_version}} + + + + + Equipment + {{host.host_equipment}} + + + + + + + +
+
+
diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts index 6ce4609..4bd57ef 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.ts +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -14,8 +14,12 @@ * limitations under the License. */ import { Component } from '@angular/core'; +import { MatTableDataSource } from '@angular/material'; +import { Host } from '../../model/host'; +import { Lab } from '../../model/lab'; import { LabService } from './lab.service'; +import { MenuBaseClass } from '../menu_base'; @Component({ @@ -24,7 +28,28 @@ import { LabService } from './lab.service'; providers: [ LabService ], styleUrls: ['./lab.component.scss'], }) -export class LabComponent { +export class LabComponent extends MenuBaseClass { + labColumnTitles = [ + '_index', + 'admin', + 'hostCount', + 'name', + 'owner', + ]; + hostColumnTitles = [ + '_index', + 'host_equipment', + 'hostname', + 'ip', + 'name', + 'vtslab_version', + ]; + labCount = -1; + constructor(private labService: LabService) { + super(); } + + labDataSource = new MatTableDataSource(); + hostDataSource = new MatTableDataSource(); } diff --git a/gae/frontend/src/app/menu/menu_base.ts b/gae/frontend/src/app/menu/menu_base.ts new file mode 100644 index 0000000..b48abfc --- /dev/null +++ b/gae/frontend/src/app/menu/menu_base.ts @@ -0,0 +1,10 @@ +export abstract class MenuBaseClass { + count = -1; + + pageSizeOptions = [20, 50, 100, 200]; + pageSize = 100; + pageIndex = 0; + + protected constructor() { + } +} diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html index 8fb9e9a..4381906 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.html +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -12,4 +12,82 @@ See the License for the specific language governing permissions and limitations under the License. --> -Schedule page +
+ + + + No. + {{i+1+pageSize*pageIndex}} + + + + + Test Name + {{schedule.test_name}} + + + + + Device + {{schedule.device.join('\n')}} + + + + + Manifest Branch + {{schedule.manifest_branch}} + + + + + Build Target + {{schedule.build_target}} + + + + + GSI Branch + {{schedule.gsi_branch}} + + + + + GSI Build Target + {{schedule.gsi_build_target}} + + + + + Test Branch + {{schedule.test_branch}} + + + + + Test Build Target + {{schedule.test_build_target}} + + + + + Period + {{schedule.period}} + + + + + Timestamp + {{schedule.timestamp}} + + + + + + + + +
diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index 69c09ac..caa4574 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -14,7 +14,10 @@ * limitations under the License. */ import { Component } from '@angular/core'; +import { MatTableDataSource, PageEvent } from '@angular/material'; +import { MenuBaseClass } from '../menu_base'; +import { Schedule } from '../../model/schedule'; import { ScheduleService } from './schedule.service'; @@ -24,7 +27,30 @@ import { ScheduleService } from './schedule.service'; providers: [ ScheduleService ], styleUrls: ['./schedule.component.scss'], }) -export class ScheduleComponent { +export class ScheduleComponent extends MenuBaseClass { + columnTitles = [ + '_index', + 'build_target', + 'device', + 'gsi_branch', + 'gsi_build_target', + 'manifest_branch', + 'period', + 'test_branch', + 'test_build_target', + 'test_name', + 'timestamp', + ]; + dataSource = new MatTableDataSource(); + pageEvent: PageEvent; + constructor(private scheduleService: ScheduleService) { + super(); + } + + onPageEvent(event: PageEvent) { + this.pageSize = event.pageSize; + this.pageIndex = event.pageIndex; + return event; } } -- cgit v1.2.3 From f7aa71f8af767fb6b3c0354cb8ef299b5c49637b Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 16 Aug 2018 14:44:03 +0900 Subject: Fix /job_queue endpoint API naming collision. Test: dev server Bug: 112449127 Change-Id: I11ffac3a99e84ee70acef801499c9b871a912e35 --- gae/webapp/src/endpoint/job_queue.py | 12 +----------- gae/webapp/src/endpoint/job_queue_test.py | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 1023f84..94fcbaa 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -41,17 +41,7 @@ class JobQueueApi(endpoint_base.EndpointBase): model.JobLeaseResponse, path='get', http_method='POST', - name='get') - def get(self, request): - # TODO(jongmok): This will be deprecated and replaced with /lease. - return self.lease(request) - - @endpoints.method( - JOB_QUEUE_RESOURCE, - model.JobLeaseResponse, - path='get', - http_method='POST', - name='get') + name='lease') def lease(self, request): """Gets the job(s) based on the condition specified in `request`.""" job_query = model.JobModel.query( diff --git a/gae/webapp/src/endpoint/job_queue_test.py b/gae/webapp/src/endpoint/job_queue_test.py index 83fb8e9..140c4f4 100644 --- a/gae/webapp/src/endpoint/job_queue_test.py +++ b/gae/webapp/src/endpoint/job_queue_test.py @@ -89,7 +89,7 @@ class JobQueueTest(unittest_base.UnitTestBase): container = (job_queue.JOB_QUEUE_RESOURCE.combined_message_class( hostname=test_values["hostname"])) api = job_queue.JobQueueApi() - response = api.get(container) + response = api.lease(container) self.assertEqual(response.return_code, model.ReturnCodeMessage.SUCCESS) -- cgit v1.2.3 From 161cf3c7fdb12154fbd491ea0e7f979ba44f499e Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 16 Aug 2018 17:44:54 +0900 Subject: Change string comparison from is not to !=. Because the request message is unicode, it requires to use != to compare strings. Test: python testing/e2e_test.py Bug: 112121209 --- gae/webapp/src/endpoint/host_info.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index 8dd01b6..75a5552 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -15,6 +15,7 @@ import datetime import endpoints +import logging from google.appengine.api import users from google.appengine.ext import ndb @@ -89,7 +90,7 @@ class HostInfoApi(endpoint_base.EndpointBase): device.serial = request_device.serial device.scheduling_status = Status.DEVICE_SCHEDULING_STATUS_DICT[ "free"] - if not device.product or request_device.product is not "error": + if not device.product or request_device.product != "error": device.product = request_device.product device.username = username -- cgit v1.2.3 From df1250123d675358d7e3922a82d53543e6272c7f Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 21 Aug 2018 11:51:03 +0900 Subject: Get items from frontend to backend via endpoints. Test: dev_appserver.py app.yaml worker.yaml && ng serve Bug: 74575555 --- gae/frontend/src/app/menu/build/build.component.ts | 64 +++++++++++++++++++++- .../src/app/menu/dashboard/dashboard.component.ts | 1 + .../src/app/menu/device/device.component.ts | 64 +++++++++++++++++++++- gae/frontend/src/app/menu/job/job.component.ts | 64 +++++++++++++++++++++- gae/frontend/src/app/menu/lab/lab.component.html | 10 ++-- gae/frontend/src/app/menu/lab/lab.component.ts | 54 ++++++++++++++++-- gae/frontend/src/app/menu/menu_base.ts | 35 ++++++++++++ .../src/app/menu/schedule/schedule.component.ts | 64 +++++++++++++++++++++- gae/frontend/src/environments/environment.prod.ts | 3 +- gae/frontend/src/environments/environment.ts | 3 +- 10 files changed, 340 insertions(+), 22 deletions(-) diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index 571ce35..f1388dc 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatTableDataSource, PageEvent } from '@angular/material'; import { Build } from '../../model/build'; import { BuildService } from './build.service'; import { MenuBaseClass } from '../menu_base'; - +/** Component that handles build menu. */ @Component({ selector: 'app-build', templateUrl: './build.component.html', providers: [ BuildService ], styleUrls: ['./build.component.scss'], }) -export class BuildComponent extends MenuBaseClass { +export class BuildComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', 'artifact_type', @@ -43,9 +43,67 @@ export class BuildComponent extends MenuBaseClass { super(); } + ngOnInit(): void { + this.getCount(); + this.getBuilds(this.pageSize, this.pageSize * this.pageIndex); + } + + /** Gets a total count of builds. */ + getCount(observer = this.getDefaultCountObservable()) { + const filterJSON = ''; + this.buildService.getCount(filterJSON).subscribe(observer); + } + + /** Gets builds. + * @param size A number, at most this many results will be returned. + * @param offset A Number of results to skip. + */ + getBuilds(size = 0, offset = 0) { + this.loading = true; + const filterJSON = ''; + this.buildService.getBuilds(size, offset, filterJSON, '', '') + .subscribe( + (response) => { + this.loading = false; + if (this.count >= 0) { + let length = 0; + if (response.body.builds) { + length = response.body.builds.length; + } + const total = length + offset; + if (response.body.has_next) { + if (length !== this.pageSize) { + console.log('Received unexpected number of entities.'); + } else if (this.count <= total) { + this.getCount(); + } + } else { + if (this.count !== total) { + if (length !== this.count) { + this.getCount(); + } else if (this.count > total) { + const countObservable = this.getDefaultCountObservable([ + () => { + this.pageIndex = Math.floor(this.count / this.pageSize); + this.getBuilds(this.pageSize, this.pageSize * this.pageIndex); + } + ]); + this.getCount(countObservable); + } + } + } + } + this.dataSource.data = response.body.builds; + }, + (error) => console.log(`[${error.status}] ${error.name}`) + ); + } + + /** Hooks a page event and handles properly. */ onPageEvent(event: PageEvent) { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; + this.getBuilds(this.pageSize, this.pageSize * this.pageIndex); return event; } } diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts index 2409b15..8094303 100644 --- a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts @@ -15,6 +15,7 @@ */ import { Component } from '@angular/core'; +/** Component that handles dashboard. */ @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index 542674c..88c86b2 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatTableDataSource, PageEvent } from '@angular/material'; import { Device } from '../../model/device'; import { DeviceService } from './device.service'; import { MenuBaseClass } from '../menu_base'; - +/** Component that handles device menu. */ @Component({ selector: 'app-device', templateUrl: './device.component.html', providers: [ DeviceService ], styleUrls: ['./device.component.scss'], }) -export class DeviceComponent extends MenuBaseClass { +export class DeviceComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', 'device_equipment', @@ -44,9 +44,67 @@ export class DeviceComponent extends MenuBaseClass { super(); } + ngOnInit(): void { + this.getCount(); + this.getDevices(this.pageSize, this.pageSize * this.pageIndex); + } + + /** Gets a total count of devices. */ + getCount(observer = this.getDefaultCountObservable()) { + const filterJSON = ''; + this.deviceService.getCount(filterJSON).subscribe(observer); + } + + /** Gets devices. + * @param size A number, at most this many results will be returned. + * @param offset A Number of results to skip. + */ + getDevices(size = 0, offset = 0) { + this.loading = true; + const filterJSON = ''; + this.deviceService.getDevices(size, offset, filterJSON, '', '') + .subscribe( + (response) => { + this.loading = false; + if (this.count >= 0) { + let length = 0; + if (response.body.devices) { + length = response.body.devices.length; + } + const total = length + offset; + if (response.body.has_next) { + if (length !== this.pageSize) { + console.log('Received unexpected number of entities.'); + } else if (this.count <= total) { + this.getCount(); + } + } else { + if (this.count !== total) { + if (length !== this.count) { + this.getCount(); + } else if (this.count > total) { + const countObservable = this.getDefaultCountObservable([ + () => { + this.pageIndex = Math.floor(this.count / this.pageSize); + this.getDevices(this.pageSize, this.pageSize * this.pageIndex); + } + ]); + this.getCount(countObservable); + } + } + } + } + this.dataSource.data = response.body.devices; + }, + (error) => console.log(`[${error.status}] ${error.name}`) + ); + } + + /** Hooks a page event and handles properly. */ onPageEvent(event: PageEvent) { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; + this.getDevices(this.pageSize, this.pageSize * this.pageIndex); return event; } } diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index cc50596..2a10d25 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatTableDataSource, PageEvent } from '@angular/material'; import { MenuBaseClass } from '../menu_base'; import { Job } from '../../model/job'; import { JobService } from './job.service'; - +/** Component that handles job menu. */ @Component({ selector: 'app-job', templateUrl: './job.component.html', providers: [ JobService ], styleUrls: ['./job.component.scss'], }) -export class JobComponent extends MenuBaseClass { +export class JobComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', 'build_id', @@ -55,9 +55,67 @@ export class JobComponent extends MenuBaseClass { super(); } + ngOnInit(): void { + this.getCount(); + this.getJobs(this.pageSize, this.pageSize * this.pageIndex); + } + + /** Gets a total count of jobs. */ + getCount(observer = this.getDefaultCountObservable()) { + const filterJSON = ''; + this.jobService.getCount(filterJSON).subscribe(observer); + } + + /** Gets jobs. + * @param size A number, at most this many results will be returned. + * @param offset A Number of results to skip. + */ + getJobs(size = 0, offset = 0) { + this.loading = true; + const filterJSON = ''; + this.jobService.getJobs(size, offset, filterJSON, '', '') + .subscribe( + (response) => { + this.loading = false; + if (this.count >= 0) { + let length = 0; + if (response.body.jobs) { + length = response.body.jobs.length; + } + const total = length + offset; + if (response.body.has_next) { + if (length !== this.pageSize) { + console.log('Received unexpected number of entities.'); + } else if (this.count <= total) { + this.getCount(); + } + } else { + if (this.count !== total) { + if (length !== this.count) { + this.getCount(); + } else if (this.count > total) { + const countObservable = this.getDefaultCountObservable([ + () => { + this.pageIndex = Math.floor(this.count / this.pageSize); + this.getJobs(this.pageSize, this.pageSize * this.pageIndex); + } + ]); + this.getCount(countObservable); + } + } + } + } + this.dataSource.data = response.body.jobs; + }, + (error) => console.log(`[${error.status}] ${error.name}`) + ); + } + + /** Hooks a page event and handles properly. */ onPageEvent(event: PageEvent) { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; + this.getJobs(this.pageSize, this.pageSize * this.pageIndex); return event; } } diff --git a/gae/frontend/src/app/menu/lab/lab.component.html b/gae/frontend/src/app/menu/lab/lab.component.html index a0701f9..29918af 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.html +++ b/gae/frontend/src/app/menu/lab/lab.component.html @@ -14,7 +14,7 @@ --> -
+
@@ -50,12 +50,13 @@ + [pageSizeOptions]="pageSizeOptions" + [pageIndex]="labPageIndex">
-
+
@@ -97,7 +98,8 @@ + [pageSizeOptions]="pageSizeOptions" + [pageIndex]="labPageIndex">
diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts index 4bd57ef..ff64fb5 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.ts +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; -import { MatTableDataSource } from '@angular/material'; +import { Component, OnInit } from '@angular/core'; +import {MatTableDataSource, PageEvent} from '@angular/material'; import { Host } from '../../model/host'; import { Lab } from '../../model/lab'; import { LabService } from './lab.service'; import { MenuBaseClass } from '../menu_base'; - +/** Component that handles lab and host menu. */ @Component({ selector: 'app-lab', templateUrl: './lab.component.html', providers: [ LabService ], styleUrls: ['./lab.component.scss'], }) -export class LabComponent extends MenuBaseClass { +export class LabComponent extends MenuBaseClass implements OnInit { labColumnTitles = [ '_index', 'admin', @@ -45,6 +45,7 @@ export class LabComponent extends MenuBaseClass { 'vtslab_version', ]; labCount = -1; + labPageIndex = 0; constructor(private labService: LabService) { super(); @@ -52,4 +53,49 @@ export class LabComponent extends MenuBaseClass { labDataSource = new MatTableDataSource(); hostDataSource = new MatTableDataSource(); + + ngOnInit(): void { + // For labs and hosts, it does not use query pagination. + this.getHosts(); + } + + /** Gets hosts. + * @param size A number, at most this many results will be returned. + * @param offset A Number of results to skip. + */ + getHosts(size = 0, offset = 0) { + this.loading = true; + // Labs will not use filter for query. + const filterJSON = ''; + this.labService.getLabs(size, offset, filterJSON, '', '') + .subscribe( + (response) => { + this.loading = false; + if (response.body.labs) { + this.count = response.body.labs.length; + this.hostDataSource.data = response.body.labs; + this.setLabs(response.body.labs); + } + }, + (error) => console.log(`[${error.status}] ${error.name}`) + ); + } + + /** Sets labs from given hosts. + * @param hosts A list of Host instances. + */ + setLabs(hosts: Host[]) { + if (hosts == null || hosts.length === 0) { return; } + const labMap = new Map(); + hosts.forEach(function(host) { + if (labMap.has(host.name)) { + labMap.get(host.name).hosts.push(host); + } else { + labMap.set(host.name, {name: host.name, owner: host.owner, admin: host.admin, hosts: [host]}); + } + }); + const labs: Lab[] = []; + labMap.forEach((value) => labs.push(value)); + this.labDataSource.data = labs; + } } diff --git a/gae/frontend/src/app/menu/menu_base.ts b/gae/frontend/src/app/menu/menu_base.ts index b48abfc..1ae5deb 100644 --- a/gae/frontend/src/app/menu/menu_base.ts +++ b/gae/frontend/src/app/menu/menu_base.ts @@ -1,10 +1,45 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** This class defines and/or implements the common properties and methods + * used among menus. + */ export abstract class MenuBaseClass { count = -1; + loading = false; pageSizeOptions = [20, 50, 100, 200]; pageSize = 100; pageIndex = 0; protected constructor() { } + + /** Returns an Observable which handles a response of count API. + * @param additionalOperations A list of lambda functions. + */ + getDefaultCountObservable(additionalOperations: any[] = []) { + return { + next: (response) => { + this.count = response.body.count; + for (const operation of additionalOperations) { + operation(response); + } + }, + error: (error) => console.log(`[${error.status}] ${error.name}`) + }; + } } diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index caa4574..1fa4629 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatTableDataSource, PageEvent } from '@angular/material'; import { MenuBaseClass } from '../menu_base'; import { Schedule } from '../../model/schedule'; import { ScheduleService } from './schedule.service'; - +/** Component that handles schedule menu. */ @Component({ selector: 'app-schedule', templateUrl: './schedule.component.html', providers: [ ScheduleService ], styleUrls: ['./schedule.component.scss'], }) -export class ScheduleComponent extends MenuBaseClass { +export class ScheduleComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', 'build_target', @@ -48,9 +48,67 @@ export class ScheduleComponent extends MenuBaseClass { super(); } + ngOnInit(): void { + this.getCount(); + this.getSchedules(this.pageSize, this.pageSize * this.pageIndex); + } + + /** Gets a total count of schedules. */ + getCount(observer = this.getDefaultCountObservable()) { + const filterJSON = ''; + this.scheduleService.getCount(filterJSON).subscribe(observer); + } + + /** Gets schedules. + * @param size A number, at most this many results will be returned. + * @param offset A Number of results to skip. + */ + getSchedules(size = 0, offset = 0) { + this.loading = true; + const filterJSON = ''; + this.scheduleService.getSchedules(size, offset, filterJSON, '', '') + .subscribe( + (response) => { + this.loading = false; + if (this.count >= 0) { + let length = 0; + if (response.body.schedules) { + length = response.body.schedules.length; + } + const total = length + offset; + if (response.body.has_next) { + if (length !== this.pageSize) { + console.log('Received unexpected number of entities.'); + } else if (this.count <= total) { + this.getCount(); + } + } else { + if (this.count !== total) { + if (length !== this.count) { + this.getCount(); + } else if (this.count > total) { + const countObservable = this.getDefaultCountObservable([ + () => { + this.pageIndex = Math.floor(this.count / this.pageSize); + this.getSchedules(this.pageSize, this.pageSize * this.pageIndex); + } + ]); + this.getCount(countObservable); + } + } + } + } + this.dataSource.data = response.body.schedules; + }, + (error) => console.log(`[${error.status}] ${error.name}`) + ); + } + + /** Hooks a page event and handles properly. */ onPageEvent(event: PageEvent) { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; + this.getSchedules(this.pageSize, this.pageSize * this.pageIndex); return event; } } diff --git a/gae/frontend/src/environments/environment.prod.ts b/gae/frontend/src/environments/environment.prod.ts index 3612073..8b051e6 100644 --- a/gae/frontend/src/environments/environment.prod.ts +++ b/gae/frontend/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + baseURL: '/_ah/api', }; diff --git a/gae/frontend/src/environments/environment.ts b/gae/frontend/src/environments/environment.ts index ffe8aed..387543e 100644 --- a/gae/frontend/src/environments/environment.ts +++ b/gae/frontend/src/environments/environment.ts @@ -1,3 +1,4 @@ export const environment = { - production: false + production: false, + baseURL: 'http://localhost:8080/_ah/api', }; -- cgit v1.2.3 From 272b6139c82a4ce0e71624c49244a264c469a50f Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 22 Aug 2018 16:01:27 +0900 Subject: Add spinner UI while loading. Test: dev_appserver.py app.yaml worker.yaml && ng serve Bug: 74575555 --- gae/frontend/src/app/app.module.ts | 23 +++++++++++++++++++--- .../src/app/menu/build/build.component.html | 5 ++++- .../src/app/menu/device/device.component.html | 5 ++++- gae/frontend/src/app/menu/job/job.component.html | 5 ++++- gae/frontend/src/app/menu/lab/lab.component.html | 3 +++ .../src/app/menu/schedule/schedule.component.html | 5 ++++- 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index 2e26033..e3e8a1c 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -22,6 +22,7 @@ import { RouterModule, Routes } from '@angular/router'; // Angular Material modules import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -48,6 +49,24 @@ const appRoutes: Routes = [ { path: '**', redirectTo: '/', pathMatch: 'full' } ]; + +@NgModule({ + imports: [ + MatPaginatorModule, + MatProgressSpinnerModule, + MatTableModule, + MatTabsModule, + ], + exports: [ + MatPaginatorModule, + MatProgressSpinnerModule, + MatTableModule, + MatTabsModule, + ] +}) +export class MaterialModule {} + + @NgModule({ declarations: [ AppComponent, @@ -62,9 +81,7 @@ const appRoutes: Routes = [ BrowserAnimationsModule, BrowserModule, HttpClientModule, - MatPaginatorModule, - MatTableModule, - MatTabsModule, + MaterialModule, NavModule, RouterModule.forRoot( appRoutes diff --git a/gae/frontend/src/app/menu/build/build.component.html b/gae/frontend/src/app/menu/build/build.component.html index fc0013a..e2b731b 100644 --- a/gae/frontend/src/app/menu/build/build.component.html +++ b/gae/frontend/src/app/menu/build/build.component.html @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
@@ -67,3 +67,6 @@ (page)="pageEvent = onPageEvent($event)">
+
+ +
diff --git a/gae/frontend/src/app/menu/device/device.component.html b/gae/frontend/src/app/menu/device/device.component.html index f70447c..cce6cf2 100644 --- a/gae/frontend/src/app/menu/device/device.component.html +++ b/gae/frontend/src/app/menu/device/device.component.html @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
@@ -67,3 +67,6 @@ (page)="pageEvent = onPageEvent($event)"> +
+ +
diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index d4b2c2e..9f073e4 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
@@ -133,3 +133,6 @@ (page)="pageEvent = onPageEvent($event)"> +
+ +
diff --git a/gae/frontend/src/app/menu/lab/lab.component.html b/gae/frontend/src/app/menu/lab/lab.component.html index a0701f9..f9b6868 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.html +++ b/gae/frontend/src/app/menu/lab/lab.component.html @@ -102,3 +102,6 @@ +
+ +
diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html index 4381906..055d7cf 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.html +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
@@ -91,3 +91,6 @@ (page)="pageEvent = onPageEvent($event)">
+
+ +
-- cgit v1.2.3 From eb8f3b619940630fa785da82b3332c8060e87f8e Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 22 Aug 2018 19:31:14 +0900 Subject: Add owner property in ScheduleModel. Test: python testing/e2e_test.py Bug: 112063908 Change-Id: I9d78b30723eaabddd2ec9ed4e2978b9ec8fb6ef7 --- gae/frontend/src/app/model/schedule.ts | 4 ++++ gae/webapp/src/proto/model.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/gae/frontend/src/app/model/schedule.ts b/gae/frontend/src/app/model/schedule.ts index f009e9e..756cb00 100644 --- a/gae/frontend/src/app/model/schedule.ts +++ b/gae/frontend/src/app/model/schedule.ts @@ -53,6 +53,10 @@ export class Schedule { report_bucket: string[] = void 0; report_spreadsheet_id: string[] = void 0; + report_persistent_url: string[] = void 0; + report_reference_url: string[] = void 0; + image_package_repo_base: string = void 0; timestamp = void 0; + owner: string[] = void 0; } diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index a354105..25af6dc 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -18,6 +18,7 @@ from google.appengine.ext import ndb from protorpc import messages +from protorpc import message_types class BuildModel(ndb.Model): @@ -102,6 +103,8 @@ class ScheduleModel(ndb.Model): report_persistent_url = ndb.StringProperty(repeated=True) report_reference_url = ndb.StringProperty(repeated=True) + owner = ndb.StringProperty(repeated=True) + class ScheduleControlInfoMessage(messages.Message): """A message for representing a schedule control data entry.""" @@ -111,7 +114,7 @@ class ScheduleControlInfoMessage(messages.Message): class ScheduleInfoMessage(messages.Message): """A message for representing an individual schedule entry.""" - # Next ID = 32 + # Next ID = 36 # schedule name for green build schedule, optional. name = messages.StringField(16) schedule_type = messages.StringField(19) @@ -124,6 +127,7 @@ class ScheduleInfoMessage(messages.Message): require_signed_device_build = messages.BooleanField(20) has_bootloader_img = messages.BooleanField(27) has_radio_img = messages.BooleanField(28) + # GSI information gsi_storage_type = messages.IntegerField(22) gsi_branch = messages.StringField(9) @@ -155,6 +159,8 @@ class ScheduleInfoMessage(messages.Message): report_reference_url = messages.StringField(33, repeated=True) image_package_repo_base = messages.StringField(31) + timestamp = message_types.DateTimeField(34) + owner = messages.StringField(35, repeated=True) class LabModel(ndb.Model): -- cgit v1.2.3 From c3232bc6296ae2ef127ec4072601c8c975fc2009 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 23 Aug 2018 18:09:07 +0900 Subject: Add logs to see job creation failure result. Test: mma Bug: 113086737 --- gae/webapp/src/scheduler/schedule_worker.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index abb919f..672e21e 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -377,6 +377,13 @@ class ScheduleHandler(webapp2.RequestHandler): model.LabModel.hostname == target_host).fetch() return CREATE_JOB_SUCCESS, labs[0].name else: + self.logger.Println("Cannot find builds to create a job.") + self.logger.Println("- Device branch / build - {} / {}".format( + new_job.manifest_branch, new_job.build_id)) + self.logger.Println("- GSI branch / build - {} / {}".format( + new_job.gsi_branch, new_job.gsi_build_id)) + self.logger.Println("- Test branch / build - {} / {}".format( + new_job.test_branch, new_job.test_build_id)) return CREATE_JOB_FAILED_NO_BUILD, "" def FilterWithPeriod(self, schedules): -- cgit v1.2.3 From 7a06567e124141df781aad4ea5e77bd89e0d459b Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 23 Aug 2018 18:05:38 +0900 Subject: Create a common Get method and add unitests. Test: python testing/e2e_test.py Bug: 112449127 Change-Id: I605d500455018ee38064d019dff749ad9a1c6342 --- gae/webapp/src/endpoint/build_info.py | 27 +--- gae/webapp/src/endpoint/endpoint_base.py | 40 ++++++ gae/webapp/src/endpoint/endpoint_base_test.py | 170 ++++++++++++++++++++++++-- gae/webapp/src/endpoint/host_info.py | 26 +--- gae/webapp/src/endpoint/job_queue.py | 26 +--- gae/webapp/src/endpoint/lab_info.py | 22 +--- gae/webapp/src/endpoint/schedule_info.py | 26 +--- 7 files changed, 216 insertions(+), 121 deletions(-) diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index e00bbd1..3d05e75 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -77,30 +77,9 @@ class BuildInfoApi(endpoint_base.EndpointBase): name="get") def get(self, request): """Gets the builds from datastore.""" - size = request.size if request.size else endpoint_base.MAX_QUERY_SIZE - offset = request.offset if request.offset else 0 - - filters = self.CreateFilterList( - filter_string=request.filter, metaclass=model.BuildModel) - - builds, more = self.Fetch( - metaclass=model.BuildModel, - size=size, - filters=filters, - offset=offset, - sort_key=request.sort, - direction=request.direction, - ) - - return_list = [] - for build in builds: - _build = {} - assigned_attributes = self.GetCommonAttributes( - resource=build, reference=model.BuildInfoMessage) - for attr in assigned_attributes: - _build[attr] = getattr(build, attr, None) - return_list.append(_build) - + return_list, more = self.Get(request=request, + metaclass=model.BuildModel, + message=model.BuildInfoMessage) return model.BuildResponseMessage(builds=return_list, has_next=more) @endpoints.method( diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py index c94346b..acf13ae 100644 --- a/gae/webapp/src/endpoint/endpoint_base.py +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -272,3 +272,43 @@ class EndpointBase(remote.Service): if _filter["key"] not in model_properties: filters.remove(_filter) return filters + + def Get(self, request, metaclass, message): + """Handles a request through /get endpoints API to retrieves entities. + + Args: + request: a request body message received through /get API. + metaclass: a metaclass for ndb model. This method will fetch the + 'metaclass' type of model from datastore. + message: a Protocol RPC message class. Fetched entities will be + converted to this message class instances. + + Returns: + a list of fetched entities. + a boolean, True if there is next page or False if not. + """ + size = request.size if request.size else MAX_QUERY_SIZE + offset = request.offset if request.offset else 0 + + filters = self.CreateFilterList( + filter_string=request.filter, metaclass=metaclass) + + entities, more = self.Fetch( + metaclass=metaclass, + size=size, + filters=filters, + offset=offset, + sort_key=request.sort, + direction=request.direction, + ) + + return_list = [] + for entity in entities: + entity_dict = {} + assigned_attributes = self.GetCommonAttributes( + resource=entity, reference=message) + for attr in assigned_attributes: + entity_dict[attr] = getattr(entity, attr, None) + return_list.append(entity_dict) + + return return_list, more diff --git a/gae/webapp/src/endpoint/endpoint_base_test.py b/gae/webapp/src/endpoint/endpoint_base_test.py index 79d6c68..2eb397a 100644 --- a/gae/webapp/src/endpoint/endpoint_base_test.py +++ b/gae/webapp/src/endpoint/endpoint_base_test.py @@ -15,6 +15,8 @@ # limitations under the License. # +import endpoints +import json import unittest try: @@ -22,25 +24,30 @@ try: except ImportError: import mock +from webapp.src import vtslab_status as Status from webapp.src.endpoint import endpoint_base from webapp.src.proto import model from webapp.src.testing import unittest_base class EndpointBaseTest(unittest_base.UnitTestBase): - """A class to test endpoint_base.EndpointBase class. """ + """A class to test endpoint_base.EndpointBase class. + + Attributes: + eb: An EndpointBase class instance. + """ def setUp(self): """Initializes test""" super(EndpointBaseTest, self).setUp() + self.eb = endpoint_base.EndpointBase() def testGetAssignedMessagesAttributes(self): attrs = ["hostname", "priority", "test_branch"] job_message = model.JobMessage() for attr in attrs: setattr(job_message, attr, attr) - eb = endpoint_base.EndpointBase() - result = eb.GetAttributes(job_message, assigned_only=True) + result = self.eb.GetAttributes(job_message, assigned_only=True) self.assertEqual(set(attrs), set(result)) def testGetAssignedModelAttributes(self): @@ -48,8 +55,7 @@ class EndpointBaseTest(unittest_base.UnitTestBase): job = model.JobModel() for attr in attrs: setattr(job, attr, attr) - eb = endpoint_base.EndpointBase() - result = eb.GetAttributes(job, assigned_only=True) + result = self.eb.GetAttributes(job, assigned_only=True) self.assertEqual(set(attrs), set(result)) def testGetAllMessagesAttributes(self): @@ -71,8 +77,7 @@ class EndpointBaseTest(unittest_base.UnitTestBase): job_message = model.JobMessage() for attr in attrs: setattr(job_message, attr, attr) - eb = endpoint_base.EndpointBase() - result = eb.GetAttributes(job_message, assigned_only=False) + result = self.eb.GetAttributes(job_message, assigned_only=False) self.assertTrue(set(full_attrs) <= set(result)) def testGetAllModelAttributes(self): @@ -95,10 +100,157 @@ class EndpointBaseTest(unittest_base.UnitTestBase): job = model.JobModel() for attr in attrs: setattr(job, attr, attr) - eb = endpoint_base.EndpointBase() - result = eb.GetAttributes(job, assigned_only=False) + result = self.eb.GetAttributes(job, assigned_only=False) self.assertTrue(set(full_attrs) <= set(result)) + def testGetSingleEntity(self): + """Asserts to get a single entity.""" + device = self.GenerateDeviceModel() + device.put() + + request_body = (endpoints.ResourceContainer( + model.GetRequestMessage).combined_message_class( + size=0, + offset=0, + filter="", + sort="", + direction="", + )) + result, more = self.eb.Get( + request=request_body, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) + self.assertEqual(len(result), 1) + self.assertFalse(more) + + def testGetHundredEntities(self): + """Asserts to get hundred entities.""" + for _ in xrange(100): + device = self.GenerateDeviceModel() + device.put() + + request_body = (endpoints.ResourceContainer( + model.GetRequestMessage).combined_message_class( + size=0, + offset=0, + filter="", + sort="", + direction="", + )) + result, more = self.eb.Get( + request=request_body, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) + self.assertEqual(len(result), 100) + self.assertFalse(more) + + def testGetEntitiesWithPagination(self): + """Asserts to get entities with pagination.""" + for _ in xrange(100): + device = self.GenerateDeviceModel() + device.put() + + request_body = (endpoints.ResourceContainer( + model.GetRequestMessage).combined_message_class( + size=60, + offset=0, + filter="", + sort="", + direction="", + )) + result, more = self.eb.Get( + request=request_body, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) + self.assertEqual(len(result), 60) + self.assertTrue(more) + + request_body = (endpoints.ResourceContainer( + model.GetRequestMessage).combined_message_class( + size=100, + offset=60, + filter="", + sort="", + direction="", + )) + result, more = self.eb.Get( + request=request_body, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) + self.assertEqual(len(result), 40) + self.assertFalse(more) + + def testGetWithFilter(self): + """Asserts to get entities with filter.""" + for _ in xrange(50): + device = self.GenerateDeviceModel() + device.put() + + for _ in xrange(50): + device = self.GenerateDeviceModel(product="product") + device.put() + + filter = [{ + "key": "product", + "method": Status.FILTER_METHOD[Status.FILTER_EqualTo], + "value": "product" + }] + filter_string = json.dumps(filter) + request_body = (endpoints.ResourceContainer( + model.GetRequestMessage).combined_message_class( + size=0, + offset=0, + filter=filter_string, + sort="", + direction="", + )) + result, more = self.eb.Get( + request=request_body, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) + self.assertEqual(len(result), 50) + self.assertFalse(more) + + def testGetWithSort(self): + """Asserts to get entities with sort.""" + for _ in xrange(100): + device = self.GenerateDeviceModel() + device.put() + + request_body = (endpoints.ResourceContainer( + model.GetRequestMessage).combined_message_class( + size=0, + offset=0, + filter="", + sort="serial", + direction="asc", + )) + + result, more = self.eb.Get( + request=request_body, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) + self.assertEqual(len(result), 100) + for i in xrange(len(result) - 1): + self.assertTrue(result[i]["serial"] < result[i + 1]["serial"]) + + request_body = (endpoints.ResourceContainer( + model.GetRequestMessage).combined_message_class( + size=0, + offset=0, + filter="", + sort="serial", + direction="desc", + )) + + result, more = self.eb.Get( + request=request_body, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) + self.assertEqual(len(result), 100) + for i in xrange(len(result) - 1): + self.assertTrue(result[i]["serial"] > result[i + 1]["serial"]) + if __name__ == "__main__": unittest.main() diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index 75a5552..d37a13d 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -112,29 +112,9 @@ class HostInfoApi(endpoint_base.EndpointBase): name="get") def get(self, request): """Gets the devices from datastore.""" - size = request.size if request.size else endpoint_base.MAX_QUERY_SIZE - offset = request.offset if request.offset else 0 - - filters = self.CreateFilterList( - filter_string=request.filter, metaclass=model.DeviceModel) - - devices, more = self.Fetch( - metaclass=model.DeviceModel, - size=size, - filters=filters, - offset=offset, - sort_key=request.sort, - direction=request.direction, - ) - - return_list = [] - for device in devices: - _device = {} - assigned_attributes = self.GetCommonAttributes( - resource=device, reference=model.DeviceInfoMessage) - for attr in assigned_attributes: - _device[attr] = getattr(device, attr, None) - return_list.append(_device) + return_list, more = self.Get(request=request, + metaclass=model.DeviceModel, + message=model.DeviceInfoMessage) return model.DeviceResponseMessage(devices=return_list, has_next=more) diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 94fcbaa..7cc4a82 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -185,29 +185,9 @@ class JobQueueApi(endpoint_base.EndpointBase): name="get") def get(self, request): """Gets the jobs from datastore.""" - size = request.size if request.size else self.MAX_QUERY_SIZE - offset = request.offset if request.offset else 0 - - filters = self.CreateFilterList( - filter_string=request.filter, metaclass=model.JobModel) - - jobs, more = self.Fetch( - metaclass=model.JobModel, - size=size, - filters=filters, - offset=offset, - sort_key=request.sort, - direction=request.direction, - ) - - return_list = [] - for job in jobs: - common_properties = self.GetCommonAttributes( - resource=job, reference=model.JobMessage) - _job = {} - for _property in common_properties: - _job[_property] = getattr(job, _property) - return_list.append(_job) + return_list, more = self.Get(request=request, + metaclass=model.JobModel, + message=model.JobMessage) return model.JobResponseMessage(jobs=return_list, has_next=more) diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 5c90554..69f6e45 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -160,25 +160,9 @@ class LabInfoApi(endpoint_base.EndpointBase): name="get") def get(self, request): """Gets the labs from datastore.""" - filters = self.CreateFilterList( - filter_string=request.filter, metaclass=model.LabModel) - - labs, more = self.Fetch( - metaclass=model.LabModel, - size=0, - filters=filters, - sort_key=request.sort, - direction=request.direction, - ) - - return_list = [] - for lab in labs: - _lab = {} - assigned_attributes = self.GetCommonAttributes( - resource=lab, reference=model.LabMessage) - for attr in assigned_attributes: - _lab[attr] = getattr(lab, attr, None) - return_list.append(_lab) + return_list, more = self.Get(request=request, + metaclass=model.LabModel, + message=model.LabMessage) return model.LabResponseMessage(labs=return_list, has_next=more) diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index be8de99..e2eaac2 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -110,29 +110,9 @@ class ScheduleInfoApi(endpoint_base.EndpointBase): name="get") def get(self, request): """Gets the schedules from datastore.""" - size = request.size if request.size else self.MAX_QUERY_SIZE - offset = request.offset if request.offset else 0 - - filters = self.CreateFilterList( - filter_string=request.filter, metaclass=model.ScheduleModel) - - schedules, more = self.Fetch( - metaclass=model.ScheduleModel, - size=size, - filters=filters, - offset=offset, - sort_key=request.sort, - direction=request.direction, - ) - - return_list = [] - for schedule in schedules: - _schedule = {} - assigned_attributes = self.GetCommonAttributes( - resource=schedule, reference=model.ScheduleInfoMessage) - for attr in assigned_attributes: - _schedule[attr] = getattr(schedule, attr, None) - return_list.append(_schedule) + return_list, more = self.Get(request=request, + metaclass=model.ScheduleModel, + message=model.ScheduleInfoMessage) return model.ScheduleResponseMessage( schedules=return_list, has_next=more) -- cgit v1.2.3 From b2738bee394f235a371f26d1b7a3b860c2ea55b8 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 29 Aug 2018 11:01:49 +0900 Subject: Apply material style UI. Test: ng serve Bug: 74575555 --- gae/frontend/angular.json | 6 ++-- .../src/app/menu/device/device.component.scss | 7 ++++ gae/frontend/src/app/menu/job/_job-theme.scss | 7 ++++ gae/frontend/src/app/menu/job/job.component.scss | 7 ++++ gae/frontend/src/app/menu/lab/lab.component.scss | 41 ++++++++++++++++++++++ .../src/app/menu/schedule/_schedule-theme.scss | 7 ++++ .../src/app/menu/schedule/schedule.component.scss | 28 +++++++++++++++ .../src/app/shared/navbar/_navbar-theme.scss | 13 +++++++ gae/frontend/src/styles.css | 0 gae/frontend/src/styles.scss | 34 ++++++++++++++++++ gae/frontend/src/styles/_app-theme.scss | 14 ++++++++ gae/frontend/src/styles/_apply-theme.scss | 5 +++ gae/frontend/src/styles/_blue-theme.scss | 34 ++++++++++++++++++ 13 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 gae/frontend/src/app/menu/job/_job-theme.scss create mode 100644 gae/frontend/src/app/menu/schedule/_schedule-theme.scss create mode 100644 gae/frontend/src/app/shared/navbar/_navbar-theme.scss delete mode 100644 gae/frontend/src/styles.css create mode 100644 gae/frontend/src/styles.scss create mode 100644 gae/frontend/src/styles/_app-theme.scss create mode 100644 gae/frontend/src/styles/_apply-theme.scss create mode 100644 gae/frontend/src/styles/_blue-theme.scss diff --git a/gae/frontend/angular.json b/gae/frontend/angular.json index 29cb325..e1c4e1a 100644 --- a/gae/frontend/angular.json +++ b/gae/frontend/angular.json @@ -23,7 +23,7 @@ "src/assets" ], "styles": [ - "src/styles.css" + "src/styles.scss" ], "scripts": [] }, @@ -72,7 +72,7 @@ "tsConfig": "src/tsconfig.spec.json", "karmaConfig": "src/karma.conf.js", "styles": [ - "src/styles.css" + "src/styles.scss" ], "scripts": [], "assets": [ @@ -124,4 +124,4 @@ } }, "defaultProject": "frontend" -} \ No newline at end of file +} diff --git a/gae/frontend/src/app/menu/device/device.component.scss b/gae/frontend/src/app/menu/device/device.component.scss index e69de29..165de43 100644 --- a/gae/frontend/src/app/menu/device/device.component.scss +++ b/gae/frontend/src/app/menu/device/device.component.scss @@ -0,0 +1,7 @@ +.mat-header-cell { + padding: 0 10px 0 10px; +} + +.mat-cell { + padding: 0 10px 0 10px; +} diff --git a/gae/frontend/src/app/menu/job/_job-theme.scss b/gae/frontend/src/app/menu/job/_job-theme.scss new file mode 100644 index 0000000..084f1fb --- /dev/null +++ b/gae/frontend/src/app/menu/job/_job-theme.scss @@ -0,0 +1,7 @@ +@mixin schedule-theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); +} diff --git a/gae/frontend/src/app/menu/job/job.component.scss b/gae/frontend/src/app/menu/job/job.component.scss index e69de29..165de43 100644 --- a/gae/frontend/src/app/menu/job/job.component.scss +++ b/gae/frontend/src/app/menu/job/job.component.scss @@ -0,0 +1,7 @@ +.mat-header-cell { + padding: 0 10px 0 10px; +} + +.mat-cell { + padding: 0 10px 0 10px; +} diff --git a/gae/frontend/src/app/menu/lab/lab.component.scss b/gae/frontend/src/app/menu/lab/lab.component.scss index e69de29..fed2fb6 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.scss +++ b/gae/frontend/src/app/menu/lab/lab.component.scss @@ -0,0 +1,41 @@ +.mat-table { + overflow: auto; +} + +.entity-table { + display: flex; + flex-direction: column; +} + +.mat-header-cell { + padding: 0 10px 0 10px; +} + +.index-column { + max-width: 40px; +} + +.mat-cell { + padding: 0 10px 0 10px; +} + +.element-row { + position: relative; + overflow: hidden; +} + +.element-row:not(.expanded) { + cursor: pointer; +} + +.element-row:not(.expanded):hover { + background: #f5f5f5; +} + +.element-row.expanded { + border-bottom-color: transparent; +} + +.div-expandable { + padding: 10px 20px 30px 20px; +} diff --git a/gae/frontend/src/app/menu/schedule/_schedule-theme.scss b/gae/frontend/src/app/menu/schedule/_schedule-theme.scss new file mode 100644 index 0000000..084f1fb --- /dev/null +++ b/gae/frontend/src/app/menu/schedule/_schedule-theme.scss @@ -0,0 +1,7 @@ +@mixin schedule-theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); +} diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.scss b/gae/frontend/src/app/menu/schedule/schedule.component.scss index e69de29..a1e3e98 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.scss +++ b/gae/frontend/src/app/menu/schedule/schedule.component.scss @@ -0,0 +1,28 @@ +.mat-header-cell { + padding: 0 10px 0 10px; +} + +.mat-cell { + padding: 0 10px 0 10px; +} + +.element-row { + position: relative; + overflow: hidden; +} + +.element-row:not(.expanded) { + cursor: pointer; +} + +.element-row:not(.expanded):hover { + background: #f5f5f5; +} + +.element-row.expanded { + border-bottom-color: transparent; +} + +.div-expandable { + padding: 10px 20px 30px 20px; +} diff --git a/gae/frontend/src/app/shared/navbar/_navbar-theme.scss b/gae/frontend/src/app/shared/navbar/_navbar-theme.scss new file mode 100644 index 0000000..bba0989 --- /dev/null +++ b/gae/frontend/src/app/shared/navbar/_navbar-theme.scss @@ -0,0 +1,13 @@ +@mixin nav-bar-theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + mat-toolbar.main-toolbar { + .mat-list-item { + color: mat-color($primary, '600-contrast') !important; + } + } +} diff --git a/gae/frontend/src/styles.css b/gae/frontend/src/styles.css deleted file mode 100644 index e69de29..0000000 diff --git a/gae/frontend/src/styles.scss b/gae/frontend/src/styles.scss new file mode 100644 index 0000000..aabb7b0 --- /dev/null +++ b/gae/frontend/src/styles.scss @@ -0,0 +1,34 @@ +@import '~@angular/material/theming'; +@import 'styles/app-theme'; + +body { + margin: 0; + font-family: Roboto, sans-serif; +} + +.entity-filter { + margin: 20px 20px 0 20px; + + filter { + width: 100%; + } +} + +.entity-table { + margin: 10px 20px 20px 20px; + + table { + width: 100%; + } +} + +.loading-spinner { + display: flex; + align-items: center; + justify-content: center; + top: 0; + left: 0; + bottom: 0; + right: 0; + position: fixed; +} diff --git a/gae/frontend/src/styles/_app-theme.scss b/gae/frontend/src/styles/_app-theme.scss new file mode 100644 index 0000000..6c18f48 --- /dev/null +++ b/gae/frontend/src/styles/_app-theme.scss @@ -0,0 +1,14 @@ +@import '~@angular/material/theming'; +@import 'apply-theme'; +@import 'blue-theme'; + +@include mat-core(); + +$main-theme: $blue-theme; + +$primary: mat-palette($main-theme, 600, 100, 900); +$accent: mat-palette($main-theme, A200, A100, A400); +$app-theme: mat-light-theme($primary, $accent); + +@include angular-material-theme($app-theme); +@include apply-theme($app-theme); diff --git a/gae/frontend/src/styles/_apply-theme.scss b/gae/frontend/src/styles/_apply-theme.scss new file mode 100644 index 0000000..fd5173f --- /dev/null +++ b/gae/frontend/src/styles/_apply-theme.scss @@ -0,0 +1,5 @@ +@import '../app/shared/navbar/navbar-theme'; + +@mixin apply-theme($theme) { + @include nav-bar-theme($theme); +} diff --git a/gae/frontend/src/styles/_blue-theme.scss b/gae/frontend/src/styles/_blue-theme.scss new file mode 100644 index 0000000..53fd744 --- /dev/null +++ b/gae/frontend/src/styles/_blue-theme.scss @@ -0,0 +1,34 @@ +@import '~@angular/material/theming'; + +$blue-theme: ( + 50: #e8f0fe, + 100: #d2e3fc, + 200: #a1c2fa, + 300: #7baaf7, + 400: #5e97f6, + 500: #4285f4, + 600: #1a73e8, + 700: #1967d2, + 800: #185abc, + 900: #174ea6, + A100: #82b1ff, + A200: #448aff, + A400: #2979ff, + A700: #2962ff, + contrast: ( + 50: #1a73e8, + 100: #1a73e8, + 200: $black-87-opacity, + 300: $black-87-opacity, + 400: $black-87-opacity, + 500: white, + 600: white, + 700: white, + 800: white, + 900: white, + A100: $black-87-opacity, + A200: white, + A400: white, + A700: white, + ) +); -- cgit v1.2.3 From 77c39a9a5f878ce51eb3f4d0396fe729258bfd75 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 29 Aug 2018 15:36:35 +0900 Subject: Check properties' existence before calling join(). Test: ng serve Bug: 74575555 --- gae/frontend/src/app/menu/job/job.component.html | 2 +- gae/frontend/src/app/menu/schedule/schedule.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 9f073e4..d8dbcca 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -47,7 +47,7 @@ Serial - {{job.serial.join('\n')}} + {{job.serial ? job.serial.join('\n') : ""}} diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html index 055d7cf..f453b23 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.html +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -29,7 +29,7 @@ Device - {{schedule.device.join('\n')}} + {{schedule.device ? schedule.device.join('\n') : ""}} -- cgit v1.2.3 From 7a8fd8555ba6b5f6d03493845504565f99069d4b Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 1 Aug 2018 16:01:12 +0900 Subject: Filter out the outdated builds from scheduling. Test: python testing/e2e_test.py Bug: 112063908 Change-Id: Ifd8eb846d57579e200b17db033a1a77780a34891 --- gae/webapp/src/endpoint/build_info.py | 18 +++++++------- gae/webapp/src/scheduler/schedule_worker.py | 7 ++++++ gae/webapp/src/scheduler/schedule_worker_test.py | 30 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index 3d05e75..0dee1a0 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -49,22 +49,20 @@ class BuildInfoApi(endpoint_base.EndpointBase): request.build_id, request.build_target, request.build_type, request.artifact_type)) - if request.signed and existing_builds: - # only signed builds need to overwrite the exist entities. + if existing_builds: build = existing_builds[0] - elif not existing_builds: - build = model.BuildModel() + if request.signed: + # only signed builds need to overwrite the exist entities. + build.signed = request.signed else: - # the same build existed and request is not signed build. - build = None - - if build: + build = model.BuildModel() common_attributes = self.GetCommonAttributes(request, model.BuildModel) for attr in common_attributes: setattr(build, attr, getattr(request, attr)) - build.timestamp = datetime.datetime.now() - build.put() + + build.timestamp = datetime.datetime.now() + build.put() return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 672e21e..03ef26b 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -168,6 +168,13 @@ class ScheduleHandler(webapp2.RequestHandler): model.BuildModel.build_type == build_type) builds = build_query.fetch() + if builds: + builds = [ + build for build in builds + if (build.timestamp > + datetime.datetime.now() - datetime.timedelta(hours=72)) + ] + if builds: self.logger.Println("-- Found build ID") builds.sort(key=lambda x: x.build_id, reverse=True) diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index f13a470..b28579d 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -15,6 +15,7 @@ # limitations under the License. # +import datetime import unittest try: @@ -518,6 +519,35 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): self.assertEqual("{}/{}".format(lab.name, device.product), ret_device) self.assertEqual([device.serial], ret_serials) + def testSimpleJobCreationWithOutdatedBuild(self): + """Asserts an outdated build is filtered out.""" + lab = self.GenerateLabModel() + lab.put() + + device = self.GenerateDeviceModel(hostname=lab.hostname) + device.put() + + schedule = self.GenerateScheduleModel( + device_model=device, lab_model=lab) + schedule.put() + + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].timestamp = datetime.datetime.now( + ) - datetime.timedelta(hours=73) + build_dict[key].put() + + self.scheduler.post() + self.assertEqual(0, len(model.JobModel.query().fetch())) + + builds = model.BuildModel().query().fetch() + for build in builds: + build.timestamp = datetime.datetime.now() + build.put() + + self.scheduler.post() + self.assertEqual(1, len(model.JobModel.query().fetch())) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 063f8c3eb642b86ebd5d579549430727c01744e2 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 3 Sep 2018 17:45:16 +0900 Subject: Change config to migrate Endpoints v2 from v1. Test: go/vtslab-schedule-dev Bug: 113777904 --- gae/app.yaml | 6 +++--- gae/requirements.txt | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/gae/app.yaml b/gae/app.yaml index 1c0d3a7..5ca4211 100644 --- a/gae/app.yaml +++ b/gae/app.yaml @@ -16,7 +16,7 @@ builtins: # [START handlers] handlers: # The endpoints handler must be mapped to /_ah/api. -- url: /_ah/spi/.* +- url: /_ah/api/.* script: webapp.src.endpoint_main.api - url: /favicon\.ico @@ -38,8 +38,8 @@ libraries: version: latest - name: pycrypto version: 2.6 -- name: endpoints - version: 1.0 +- name: ssl + version: 2.7.11 # [END libraries] # [START exclude] diff --git a/gae/requirements.txt b/gae/requirements.txt index 760533d..e8c00fc 100644 --- a/gae/requirements.txt +++ b/gae/requirements.txt @@ -1,8 +1,5 @@ google-api-python-client - -# The below packages are needed to build locally. -# google-endpoints==2.4.5 -# google-endpoints-api-management==1.3.0 +google-endpoints pytz stripe -- cgit v1.2.3 From 5a888b18323d93fca4782947ef17788b139cbaac Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 3 Sep 2018 17:48:35 +0900 Subject: Change Endpoints API name. "Google Cloud Endpoints API names must match the regular expression [a-z][a-z0-9]{0,39}." Test: go/vtslab-schedule-dev Bug: 113777904 --- gae/webapp/src/endpoint/build_info.py | 2 +- gae/webapp/src/endpoint/host_info.py | 2 +- gae/webapp/src/endpoint/job_queue.py | 2 +- gae/webapp/src/endpoint/lab_info.py | 2 +- gae/webapp/src/endpoint/schedule_info.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index 3d05e75..cb3eabb 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -23,7 +23,7 @@ from webapp.src.proto import model BUILD_INFO_RESOURCE = endpoints.ResourceContainer(model.BuildInfoMessage) -@endpoints.api(name="build_info", version="v1") +@endpoints.api(name="build", version="v1") class BuildInfoApi(endpoint_base.EndpointBase): """Endpoint API for build_info.""" diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index d37a13d..89266d5 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -60,7 +60,7 @@ def AddNullDevices(hostname, null_device_count): ndb.put_multi(devices_to_put) -@endpoints.api(name='host_info', version='v1') +@endpoints.api(name='host', version='v1') class HostInfoApi(endpoint_base.EndpointBase): """Endpoint API for host_info.""" diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 7cc4a82..9cde468 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -32,7 +32,7 @@ HTTP_HTTPS_REGEX = "^https?://" STORAGE_API_URL = "https://storage.cloud.google.com/" -@endpoints.api(name='job_queue', version='v1') +@endpoints.api(name='job', version='v1') class JobQueueApi(endpoint_base.EndpointBase): """Endpoint API for job_queue.""" diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 69f6e45..f0c861f 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -28,7 +28,7 @@ LAB_INFO_RESOURCE = endpoints.ResourceContainer(model.LabInfoMessage) LAB_HOST_INFO_RESOURCE = endpoints.ResourceContainer(model.LabHostInfoMessage) -@endpoints.api(name='lab_info', version='v1') +@endpoints.api(name='lab', version='v1') class LabInfoApi(endpoint_base.EndpointBase): """Endpoint API for lab_info.""" diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index e2eaac2..828a4c6 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -25,7 +25,7 @@ from webapp.src.proto import model SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer(model.ScheduleInfoMessage) -@endpoints.api(name="schedule_info", version="v1") +@endpoints.api(name="schedule", version="v1") class ScheduleInfoApi(endpoint_base.EndpointBase): """Endpoint API for schedule_info.""" -- cgit v1.2.3 From e68d17a4afec039be76955ef358d3414beb726f4 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 3 Sep 2018 17:52:28 +0900 Subject: Change GET methods API to POST methods. GET methods with body are not supported when generating OpenAPI documents, so they are changed to POST. Test: go/vtslab-schedule-dev Bug: 113777904 --- gae/webapp/src/endpoint/build_info.py | 4 ++-- gae/webapp/src/endpoint/host_info.py | 4 ++-- gae/webapp/src/endpoint/job_queue.py | 6 +++--- gae/webapp/src/endpoint/lab_info.py | 4 ++-- gae/webapp/src/endpoint/schedule_info.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gae/webapp/src/endpoint/build_info.py b/gae/webapp/src/endpoint/build_info.py index 3d05e75..eb9db9e 100644 --- a/gae/webapp/src/endpoint/build_info.py +++ b/gae/webapp/src/endpoint/build_info.py @@ -73,7 +73,7 @@ class BuildInfoApi(endpoint_base.EndpointBase): endpoint_base.GET_REQUEST_RESOURCE, model.BuildResponseMessage, path="get", - http_method="GET", + http_method="POST", name="get") def get(self, request): """Gets the builds from datastore.""" @@ -86,7 +86,7 @@ class BuildInfoApi(endpoint_base.EndpointBase): endpoint_base.COUNT_REQUEST_RESOURCE, model.CountResponseMessage, path="count", - http_method="GET", + http_method="POST", name="count") def count(self, request): """Gets total number of BuildModel entities stored in datastore.""" diff --git a/gae/webapp/src/endpoint/host_info.py b/gae/webapp/src/endpoint/host_info.py index d37a13d..550219b 100644 --- a/gae/webapp/src/endpoint/host_info.py +++ b/gae/webapp/src/endpoint/host_info.py @@ -108,7 +108,7 @@ class HostInfoApi(endpoint_base.EndpointBase): endpoint_base.GET_REQUEST_RESOURCE, model.DeviceResponseMessage, path="get", - http_method="GET", + http_method="POST", name="get") def get(self, request): """Gets the devices from datastore.""" @@ -122,7 +122,7 @@ class HostInfoApi(endpoint_base.EndpointBase): endpoint_base.COUNT_REQUEST_RESOURCE, model.CountResponseMessage, path="count", - http_method="GET", + http_method="POST", name="count") def count(self, request): """Gets total number of DeviceModel entities stored in datastore.""" diff --git a/gae/webapp/src/endpoint/job_queue.py b/gae/webapp/src/endpoint/job_queue.py index 7cc4a82..633c73d 100644 --- a/gae/webapp/src/endpoint/job_queue.py +++ b/gae/webapp/src/endpoint/job_queue.py @@ -39,7 +39,7 @@ class JobQueueApi(endpoint_base.EndpointBase): @endpoints.method( JOB_QUEUE_RESOURCE, model.JobLeaseResponse, - path='get', + path='lease', http_method='POST', name='lease') def lease(self, request): @@ -181,7 +181,7 @@ class JobQueueApi(endpoint_base.EndpointBase): endpoint_base.GET_REQUEST_RESOURCE, model.JobResponseMessage, path="get", - http_method="GET", + http_method="POST", name="get") def get(self, request): """Gets the jobs from datastore.""" @@ -195,7 +195,7 @@ class JobQueueApi(endpoint_base.EndpointBase): endpoint_base.COUNT_REQUEST_RESOURCE, model.CountResponseMessage, path="count", - http_method="GET", + http_method="POST", name="count") def count(self, request): """Gets total number of JobModel entities stored in datastore.""" diff --git a/gae/webapp/src/endpoint/lab_info.py b/gae/webapp/src/endpoint/lab_info.py index 69f6e45..afcf58d 100644 --- a/gae/webapp/src/endpoint/lab_info.py +++ b/gae/webapp/src/endpoint/lab_info.py @@ -156,7 +156,7 @@ class LabInfoApi(endpoint_base.EndpointBase): endpoint_base.GET_REQUEST_RESOURCE, model.LabResponseMessage, path="get", - http_method="GET", + http_method="POST", name="get") def get(self, request): """Gets the labs from datastore.""" @@ -170,7 +170,7 @@ class LabInfoApi(endpoint_base.EndpointBase): endpoint_base.COUNT_REQUEST_RESOURCE, model.CountResponseMessage, path="count", - http_method="GET", + http_method="POST", name="count") def count(self, request): """Gets total number of BuildModel entities stored in datastore.""" diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index e2eaac2..44535b7 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -106,7 +106,7 @@ class ScheduleInfoApi(endpoint_base.EndpointBase): endpoint_base.GET_REQUEST_RESOURCE, model.ScheduleResponseMessage, path="get", - http_method="GET", + http_method="POST", name="get") def get(self, request): """Gets the schedules from datastore.""" @@ -121,7 +121,7 @@ class ScheduleInfoApi(endpoint_base.EndpointBase): endpoint_base.COUNT_REQUEST_RESOURCE, model.CountResponseMessage, path="count", - http_method="GET", + http_method="POST", name="count") def count(self, request): """Gets total number of ScheduleModel entities stored in datastore.""" -- cgit v1.2.3 From 829d7d497d87027b4a06580dddcfe22265d6c035 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 3 Sep 2018 18:09:15 +0900 Subject: Change HTTP methods from GET to POST. Test: dev_appserver.py app.yaml worker.yaml && ng serve Bug: 113777904 --- gae/frontend/src/app/menu/build/build.component.ts | 8 ++++---- gae/frontend/src/app/menu/build/build.service.ts | 13 ++++--------- gae/frontend/src/app/menu/device/device.component.ts | 8 ++++---- gae/frontend/src/app/menu/device/device.service.ts | 13 ++++--------- gae/frontend/src/app/menu/job/job.component.ts | 8 ++++---- gae/frontend/src/app/menu/job/job.service.ts | 13 ++++--------- gae/frontend/src/app/menu/lab/lab.component.ts | 8 ++++---- gae/frontend/src/app/menu/lab/lab.service.ts | 13 ++++--------- gae/frontend/src/app/menu/menu_base.ts | 2 +- gae/frontend/src/app/menu/schedule/schedule.component.ts | 8 ++++---- gae/frontend/src/app/menu/schedule/schedule.service.ts | 13 ++++--------- gae/frontend/src/app/shared/servicebase.ts | 6 ++---- 12 files changed, 43 insertions(+), 70 deletions(-) diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index f1388dc..8ebe22a 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -67,11 +67,11 @@ export class BuildComponent extends MenuBaseClass implements OnInit { this.loading = false; if (this.count >= 0) { let length = 0; - if (response.body.builds) { - length = response.body.builds.length; + if (response.builds) { + length = response.builds.length; } const total = length + offset; - if (response.body.has_next) { + if (response.has_next) { if (length !== this.pageSize) { console.log('Received unexpected number of entities.'); } else if (this.count <= total) { @@ -93,7 +93,7 @@ export class BuildComponent extends MenuBaseClass implements OnInit { } } } - this.dataSource.data = response.body.builds; + this.dataSource.data = response.builds; }, (error) => console.log(`[${error.status}] ${error.name}`) ); diff --git a/gae/frontend/src/app/menu/build/build.service.ts b/gae/frontend/src/app/menu/build/build.service.ts index d3e4c6f..d60f790 100644 --- a/gae/frontend/src/app/menu/build/build.service.ts +++ b/gae/frontend/src/app/menu/build/build.service.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { catchError } from 'rxjs/operators'; @@ -28,21 +28,16 @@ import { ServiceBase } from '../../shared/servicebase'; export class BuildService extends ServiceBase { constructor(public httpClient: HttpClient) { super(httpClient); - this.url = environment['baseURL'] + '/build_info/v1/'; + this.url = environment['baseURL'] + '/build/v1/'; } getBuilds(size: number, offset: number, filterInfo: string, sort: string, - direction: string): Observable> { + direction: string): Observable { const url = this.url + 'get'; - return this.httpClient.get(url, {observe: 'response', params: new HttpParams() - .append('size', String(size)) - .append('offset', String(offset)) - .append('filter', filterInfo) - .append('sort', sort) - .append('direction', direction)}) + return this.httpClient.post(url, {size: size, offset: offset, filter: filterInfo, sort: sort, direction: direction}) .pipe(catchError(this.handleError)); } } diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index 88c86b2..cec9f76 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -68,11 +68,11 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { this.loading = false; if (this.count >= 0) { let length = 0; - if (response.body.devices) { - length = response.body.devices.length; + if (response.devices) { + length = response.devices.length; } const total = length + offset; - if (response.body.has_next) { + if (response.has_next) { if (length !== this.pageSize) { console.log('Received unexpected number of entities.'); } else if (this.count <= total) { @@ -94,7 +94,7 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { } } } - this.dataSource.data = response.body.devices; + this.dataSource.data = response.devices; }, (error) => console.log(`[${error.status}] ${error.name}`) ); diff --git a/gae/frontend/src/app/menu/device/device.service.ts b/gae/frontend/src/app/menu/device/device.service.ts index 2223bd4..2a465ff 100644 --- a/gae/frontend/src/app/menu/device/device.service.ts +++ b/gae/frontend/src/app/menu/device/device.service.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { catchError } from 'rxjs/operators'; @@ -28,21 +28,16 @@ import { ServiceBase } from '../../shared/servicebase'; export class DeviceService extends ServiceBase { constructor(public httpClient: HttpClient) { super(httpClient); - this.url = environment['baseURL'] + '/host_info/v1/'; + this.url = environment['baseURL'] + '/host/v1/'; } getDevices(size: number, offset: number, filterInfo: string, sort: string, - direction: string): Observable> { + direction: string): Observable { const url = this.url + 'get'; - return this.httpClient.get(url, {observe: 'response', params: new HttpParams() - .append('size', String(size)) - .append('offset', String(offset)) - .append('filter', filterInfo) - .append('sort', sort) - .append('direction', direction)}) + return this.httpClient.post(url, {size: size, offset: offset, filter: filterInfo, sort: sort, direction: direction}) .pipe(catchError(this.handleError)); } } diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index 2a10d25..d3caccf 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -79,11 +79,11 @@ export class JobComponent extends MenuBaseClass implements OnInit { this.loading = false; if (this.count >= 0) { let length = 0; - if (response.body.jobs) { - length = response.body.jobs.length; + if (response.jobs) { + length = response.jobs.length; } const total = length + offset; - if (response.body.has_next) { + if (response.has_next) { if (length !== this.pageSize) { console.log('Received unexpected number of entities.'); } else if (this.count <= total) { @@ -105,7 +105,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { } } } - this.dataSource.data = response.body.jobs; + this.dataSource.data = response.jobs; }, (error) => console.log(`[${error.status}] ${error.name}`) ); diff --git a/gae/frontend/src/app/menu/job/job.service.ts b/gae/frontend/src/app/menu/job/job.service.ts index 3a99d72..71b5417 100644 --- a/gae/frontend/src/app/menu/job/job.service.ts +++ b/gae/frontend/src/app/menu/job/job.service.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { catchError } from 'rxjs/operators'; @@ -29,21 +29,16 @@ export class JobService extends ServiceBase { // url: string; constructor(public httpClient: HttpClient) { super(httpClient); - this.url = environment['baseURL'] + '/job_queue/v1/'; + this.url = environment['baseURL'] + '/job/v1/'; } getJobs(size: number, offset: number, filterInfo: string, sort: string, - direction: string): Observable> { + direction: string): Observable { const url = this.url + 'get'; - return this.httpClient.get(url, {observe: 'response', params: new HttpParams() - .append('size', String(size)) - .append('offset', String(offset)) - .append('filter', filterInfo) - .append('sort', sort) - .append('direction', direction)}) + return this.httpClient.post(url, {size: size, offset: offset, filter: filterInfo, sort: sort, direction: direction}) .pipe(catchError(this.handleError)); } } diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts index ff64fb5..db9dc09 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.ts +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -71,10 +71,10 @@ export class LabComponent extends MenuBaseClass implements OnInit { .subscribe( (response) => { this.loading = false; - if (response.body.labs) { - this.count = response.body.labs.length; - this.hostDataSource.data = response.body.labs; - this.setLabs(response.body.labs); + if (response.labs) { + this.count = response.labs.length; + this.hostDataSource.data = response.labs; + this.setLabs(response.labs); } }, (error) => console.log(`[${error.status}] ${error.name}`) diff --git a/gae/frontend/src/app/menu/lab/lab.service.ts b/gae/frontend/src/app/menu/lab/lab.service.ts index 0fda86d..2d677b1 100644 --- a/gae/frontend/src/app/menu/lab/lab.service.ts +++ b/gae/frontend/src/app/menu/lab/lab.service.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { catchError } from 'rxjs/operators'; @@ -29,21 +29,16 @@ export class LabService extends ServiceBase { // url: string; constructor(public httpClient: HttpClient) { super(httpClient); - this.url = environment['baseURL'] + '/lab_info/v1/'; + this.url = environment['baseURL'] + '/lab/v1/'; } getLabs(size: number, offset: number, filterInfo: string, sort: string, - direction: string): Observable> { + direction: string): Observable { const url = this.url + 'get'; - return this.httpClient.get(url, {observe: 'response', params: new HttpParams() - .append('size', String(size)) - .append('offset', String(offset)) - .append('filter', filterInfo) - .append('sort', sort) - .append('direction', direction)}) + return this.httpClient.post(url, {size: size, offset: offset, filter: filterInfo, sort: sort, direction: direction}) .pipe(catchError(this.handleError)); } } diff --git a/gae/frontend/src/app/menu/menu_base.ts b/gae/frontend/src/app/menu/menu_base.ts index 1ae5deb..f34bad2 100644 --- a/gae/frontend/src/app/menu/menu_base.ts +++ b/gae/frontend/src/app/menu/menu_base.ts @@ -34,7 +34,7 @@ export abstract class MenuBaseClass { getDefaultCountObservable(additionalOperations: any[] = []) { return { next: (response) => { - this.count = response.body.count; + this.count = response.count; for (const operation of additionalOperations) { operation(response); } diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index 1fa4629..87305fe 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -72,11 +72,11 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { this.loading = false; if (this.count >= 0) { let length = 0; - if (response.body.schedules) { - length = response.body.schedules.length; + if (response.schedules) { + length = response.schedules.length; } const total = length + offset; - if (response.body.has_next) { + if (response.has_next) { if (length !== this.pageSize) { console.log('Received unexpected number of entities.'); } else if (this.count <= total) { @@ -98,7 +98,7 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { } } } - this.dataSource.data = response.body.schedules; + this.dataSource.data = response.schedules; }, (error) => console.log(`[${error.status}] ${error.name}`) ); diff --git a/gae/frontend/src/app/menu/schedule/schedule.service.ts b/gae/frontend/src/app/menu/schedule/schedule.service.ts index cda44f9..86c831d 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.service.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.service.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { catchError } from 'rxjs/operators'; @@ -29,21 +29,16 @@ export class ScheduleService extends ServiceBase { // url: string; constructor(public httpClient: HttpClient) { super(httpClient); - this.url = environment['baseURL'] + '/schedule_info/v1/'; + this.url = environment['baseURL'] + '/schedule/v1/'; } getSchedules(size: number, offset: number, filterInfo: string, sort: string, - direction: string): Observable> { + direction: string): Observable { const url = this.url + 'get'; - return this.httpClient.get(url, {observe: 'response', params: new HttpParams() - .append('size', String(size)) - .append('offset', String(offset)) - .append('filter', filterInfo) - .append('sort', sort) - .append('direction', direction)}) + return this.httpClient.post(url, {size: size, offset: offset, filter: filterInfo, sort: sort, direction: direction}) .pipe(catchError(this.handleError)); } } diff --git a/gae/frontend/src/app/shared/servicebase.ts b/gae/frontend/src/app/shared/servicebase.ts index f7b8c8f..94a4f67 100644 --- a/gae/frontend/src/app/shared/servicebase.ts +++ b/gae/frontend/src/app/shared/servicebase.ts @@ -35,10 +35,8 @@ export class ServiceBase { return throwError( 'Something bad happened; please try again later.'); } - public getCount(filterInfo: string): Observable> { + public getCount(filterInfo: string): Observable { const url = this.url + 'count'; - return this.httpClient.get(url, {observe: 'response', params: new HttpParams() - .append('filter', filterInfo) - }); + return this.httpClient.post(url, {filter: filterInfo}); } } -- cgit v1.2.3 From 5bd1cfe592a31aa1f73f861bcb17450a849bd623 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 3 Sep 2018 18:17:07 +0900 Subject: Update a script to write OpenAPI documents. Test: ./script/deploy-endpoint.sh test Bug: 113777904 Change-Id: Ic5cce4bc62ebe6e173d2d433f9db1c2bc53b39fc --- gae/script/deploy-endpoint.sh | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/gae/script/deploy-endpoint.sh b/gae/script/deploy-endpoint.sh index 45ce9ac..8c4e1fd 100755 --- a/gae/script/deploy-endpoint.sh +++ b/gae/script/deploy-endpoint.sh @@ -20,21 +20,19 @@ if [ "$#" -ne 1 ]; then fi if [ $1 = "prod" ]; then - SERVICE="vtslab-schedule-prod.appspot.com" + SERVICE="vtslab-schedule-prod" elif [ $1 = "public" ]; then - SERVICE="vtslab-schedule.appspot.com" + SERVICE="vtslab-schedule" else - SERVICE="vtslab-schedule-test.appspot.com" + SERVICE="vtslab-schedule-test" fi +echo "Creating OpenAPI spec files for $SERVICE.appspot.com ..." +python lib/endpoints/endpointscfg.py get_openapi_spec webapp.src.endpoint.build_info.BuildInfoApi webapp.src.endpoint.host_info.HostInfoApi webapp.src.endpoint.lab_info.LabInfoApi webapp.src.endpoint.schedule_info.ScheduleInfoApi webapp.src.endpoint.job_queue.JobQueueApi --hostname $SERVICE.appspot.com --x-google-api-name + echo "Depolying the endpoint API implementation to $SERVICE ..." -gcloud endpoints services deploy build_infov1openapi.json -gcloud endpoints services deploy host_infov1openapi.json -gcloud endpoints services deploy lab_infov1openapi.json -gcloud endpoints services deploy schedule_infov1openapi.json +gcloud endpoints services deploy buildv1openapi.json hostv1openapi.json labv1openapi.json schedulev1openapi.json jobv1openapi.json --project=$SERVICE gcloud endpoints configs list --service=$SERVICE echo "Deployment done!" - -vi app.yaml -- cgit v1.2.3 From 7c531606309e2509b5a22ab9add09515fefc1547 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 3 Sep 2018 18:52:57 +0900 Subject: Update a deploy script to fetch endpoints version. Test: ./script/deploy-webapp.sh test Bug: 113777904 Change-Id: I45b4f1cd5cec39768ecc75d31c1339fec0eb20b5 --- gae/app.yaml | 4 ++-- gae/script/deploy-webapp.sh | 33 +++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/gae/app.yaml b/gae/app.yaml index 1c0d3a7..ba6e4a9 100644 --- a/gae/app.yaml +++ b/gae/app.yaml @@ -4,8 +4,8 @@ threadsafe: true # [START env_vars] env_variables: - ENDPOINTS_SERVICE_NAME: vtslab-schedule-prod.appspot.com - ENDPOINTS_SERVICE_VERSION: 2018-02-01r2 + ENDPOINTS_SERVICE_NAME: + ENDPOINTS_SERVICE_VERSION: SESSION_SECRET_KEY: '' # [END env_vars] diff --git a/gae/script/deploy-webapp.sh b/gae/script/deploy-webapp.sh index 7b3856c..f3cc91e 100755 --- a/gae/script/deploy-webapp.sh +++ b/gae/script/deploy-webapp.sh @@ -19,15 +19,36 @@ if [ "$#" -ne 1 ]; then exit 1 fi -if [ $1 = "prod" ]; then - SERVICE="vtslab-schedule-prod" -elif [ $1 = "test" ]; then - SERVICE="vtslab-schedule-test" -elif [ $1 = "public" ]; then +if [ $1 = "public" ]; then SERVICE="vtslab-schedule" -else +elif [ $1 = "local" ]; then dev_appserver.py ./ exit 0 +else + SERVICE="vtslab-schedule-$1" +fi + +echo "Fetching endpoints service version of $SERVICE ..." +ENDPOINTS=$(gcloud endpoints configs list --service=$SERVICE.appspot.com) +arr=($ENDPOINTS) + +if [ ${#arr[@]} -lt 4 ]; then + echo "You need to deploy endpoints first." + exit 0 +else + VERSION=${arr[2]} + NAME=${arr[3]} + echo "ENDPOINTS_SERVICE_NAME: $NAME" + echo "ENDPOINTS_SERVICE_VERSION: $VERSION" +fi + +echo "Updating app.yaml ..." +if [ "$(uname)" == "Darwin" ]; then + sed -i "" "s/ENDPOINTS_SERVICE_NAME:.*/ENDPOINTS_SERVICE_NAME: $NAME/g" app.yaml + sed -i "" "s/ENDPOINTS_SERVICE_VERSION:.*/ENDPOINTS_SERVICE_VERSION: $VERSION/g" app.yaml +else + sed -i "s/ENDPOINTS_SERVICE_NAME:.*/ENDPOINTS_SERVICE_NAME: $NAME/g" app.yaml + sed -i "s/ENDPOINTS_SERVICE_VERSION:.*/ENDPOINTS_SERVICE_VERSION: $VERSION/g" app.yaml fi echo "Deploying the web app to $SERVICE ..." -- cgit v1.2.3 From 1999b72fc622e9792966de599a08286f34997d7a Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 1 Aug 2018 15:36:58 +0900 Subject: Filter out the outdated schedules from scheduling. Test: python testing/e2e_test.py Bug: 112063908 Change-Id: I69cb8c25e4008510ac2abedd0295fcb8a66eb978 --- gae/webapp/src/endpoint/schedule_info.py | 12 +++++---- gae/webapp/src/scheduler/schedule_worker.py | 6 +++++ gae/webapp/src/scheduler/schedule_worker_test.py | 32 ++++++++++++++++++++++-- gae/webapp/src/testing/unittest_base.py | 4 ++- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index e2eaac2..4a4bf5a 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -86,18 +86,20 @@ class ScheduleInfoApi(endpoint_base.EndpointBase): [not getattr(schedule, attr) for attr in empty_list_field]) ] - if not duplicated_schedules: + if duplicated_schedules: + schedule = duplicated_schedules[0] + else: schedule = model.ScheduleModel() for attr_name in exist_on_both: setattr(schedule, attr_name, request.get_assigned_value(attr_name)) - schedule.timestamp = datetime.datetime.now() schedule.schedule_type = "test" schedule.error_count = 0 schedule.suspended = False - schedule.priority_value = Status.GetPriorityValue( - schedule.priority) - schedule.put() + schedule.priority_value = Status.GetPriorityValue(schedule.priority) + + schedule.timestamp = datetime.datetime.now() + schedule.put() return model.DefaultResponse( return_code=model.ReturnCodeMessage.SUCCESS) diff --git a/gae/webapp/src/scheduler/schedule_worker.py b/gae/webapp/src/scheduler/schedule_worker.py index 03ef26b..4c4b20f 100644 --- a/gae/webapp/src/scheduler/schedule_worker.py +++ b/gae/webapp/src/scheduler/schedule_worker.py @@ -198,6 +198,12 @@ class ScheduleHandler(webapp2.RequestHandler): schedules = schedule_query.fetch() if schedules: + # filter out the schedules which are not updated within 72 hours. + schedules = [ + schedule for schedule in schedules + if (schedule.timestamp > + datetime.datetime.now() - datetime.timedelta(hours=72)) + ] schedules = self.FilterWithPeriod(schedules) if schedules: diff --git a/gae/webapp/src/scheduler/schedule_worker_test.py b/gae/webapp/src/scheduler/schedule_worker_test.py index b28579d..2dcf1e9 100644 --- a/gae/webapp/src/scheduler/schedule_worker_test.py +++ b/gae/webapp/src/scheduler/schedule_worker_test.py @@ -508,8 +508,8 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): device = self.GenerateDeviceModel(hostname=lab.hostname) device.put() - schedule = self.GenerateScheduleModel(device_model=device, - lab_model=lab) + schedule = self.GenerateScheduleModel( + device_model=device, lab_model=lab) schedule.put() ret_host, ret_device, ret_serials = ( @@ -548,6 +548,34 @@ class ScheduleHandlerTest(unittest_base.UnitTestBase): self.scheduler.post() self.assertEqual(1, len(model.JobModel.query().fetch())) + def testSimpleJobCreationWithOutdatedSchedule(self): + """Asserts an outdated schedule is filtered out.""" + lab = self.GenerateLabModel() + lab.put() + + device = self.GenerateDeviceModel(hostname=lab.hostname) + device.put() + + schedule = self.GenerateScheduleModel( + device_model=device, lab_model=lab) + schedule.timestamp = datetime.datetime.now() - datetime.timedelta( + hours=73) + schedule.put() + + build_dict = self.GenerateBuildModel(schedule) + for key in build_dict: + build_dict[key].put() + + self.scheduler.post() + self.assertEqual(0, len(model.JobModel.query().fetch())) + + schedule = model.ScheduleModel().query().fetch()[0] + schedule.timestamp = datetime.datetime.now() + schedule.put() + + self.scheduler.post() + self.assertEqual(1, len(model.JobModel.query().fetch())) + if __name__ == "__main__": unittest.main() diff --git a/gae/webapp/src/testing/unittest_base.py b/gae/webapp/src/testing/unittest_base.py index 04981b4..0e47ee0 100644 --- a/gae/webapp/src/testing/unittest_base.py +++ b/gae/webapp/src/testing/unittest_base.py @@ -216,13 +216,15 @@ class UnitTestBase(unittest.TestCase): schedule.device = [] schedule.device.append("/".join([lab, device_product])) + schedule.timestamp = datetime.datetime.now() + skip_list = [ "priority", "priority_value", "period", "shards", "retry_count", "required_signed_device_build", "build_storage_type", "manifest_branch", "build_target", "gsi_storage_type", "gsi_build_target", "test_storage_type", "test_build_target", "device", - "children_jobs", "timestamp"] + "children_jobs"] set_or_empty = ["required_host_equipment", "required_device_equipment"] for arg in schedule._properties: if arg in skip_list or (arg in set_or_empty and arg not in kwargs): -- cgit v1.2.3 From 49f79237d2a03b44ebc16bfa97f38674b2a49ce1 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 6 Sep 2018 11:05:49 +0900 Subject: Update endpoint version to 2018-09-03r0. These values cannot be None(empty) to run dev_appserver.py. Test: dev_appserver.py app.yaml Bug: 113777904 --- gae/app.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gae/app.yaml b/gae/app.yaml index 6fd5a9b..c2b8dfe 100644 --- a/gae/app.yaml +++ b/gae/app.yaml @@ -4,8 +4,8 @@ threadsafe: true # [START env_vars] env_variables: - ENDPOINTS_SERVICE_NAME: - ENDPOINTS_SERVICE_VERSION: + ENDPOINTS_SERVICE_NAME: vtslab-schedule-prod.appspot.com + ENDPOINTS_SERVICE_VERSION: 2018-09-03r0 SESSION_SECRET_KEY: '' # [END env_vars] -- cgit v1.2.3 From d3ee37bffb157438d6cf9c41e439337ef9e3fc55 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 6 Sep 2018 16:56:42 +0900 Subject: Update OpenAPI documents' name. Test: mma Bug: 113777904 --- gae/build_infov1openapi.json | 92 ----- gae/buildv1openapi.json | 194 +++++++++++ gae/host_infov1openapi.json | 100 ------ gae/hostv1openapi.json | 243 ++++++++++++++ gae/jobv1openapi.json | 718 ++++++++++++++++++++++++++++++++++++++++ gae/lab_infov1openapi.json | 118 ------- gae/labv1openapi.json | 408 +++++++++++++++++++++++ gae/schedule_infov1openapi.json | 147 -------- gae/schedulev1openapi.json | 545 ++++++++++++++++++++++++++++++ 9 files changed, 2108 insertions(+), 457 deletions(-) delete mode 100644 gae/build_infov1openapi.json create mode 100644 gae/buildv1openapi.json delete mode 100644 gae/host_infov1openapi.json create mode 100644 gae/hostv1openapi.json create mode 100644 gae/jobv1openapi.json delete mode 100644 gae/lab_infov1openapi.json create mode 100644 gae/labv1openapi.json delete mode 100644 gae/schedule_infov1openapi.json create mode 100644 gae/schedulev1openapi.json diff --git a/gae/build_infov1openapi.json b/gae/build_infov1openapi.json deleted file mode 100644 index 2cda81e..0000000 --- a/gae/build_infov1openapi.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "basePath": "/_ah/api", - "consumes": [ - "application/json" - ], - "definitions": { - "WebappSrcProtoModelBuildInfoMessage": { - "properties": { - "artifact_type": { - "type": "string" - }, - "artifacts": { - "items": { - "type": "string" - }, - "type": "array" - }, - "build_id": { - "type": "string" - }, - "build_target": { - "type": "string" - }, - "build_type": { - "type": "string" - }, - "manifest_branch": { - "type": "string" - } - }, - "type": "object" - }, - "WebappSrcProtoModelDefaultResponse": { - "properties": { - "return_code": { - "enum": [ - "SUCCESS", - "FAIL" - ], - "type": "string" - } - }, - "type": "object" - } - }, - "host": "vtslab-schedule-prod.appspot.com", - "info": { - "description": "Endpoint API for build_info.", - "title": "build_info", - "version": "v1" - }, - "paths": { - "/build_info/v1/set": { - "post": { - "operationId": "BuildInfoApi_set", - "parameters": [ - { - "in": "body", - "name": "body", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelBuildInfoMessage" - } - } - ], - "responses": { - "200": { - "description": "A successful response", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" - } - } - } - } - } - }, - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "securityDefinitions": { - "google_id_token": { - "authorizationUrl": "", - "flow": "implicit", - "type": "oauth2", - "x-google-issuer": "https://accounts.google.com", - "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" - } - }, - "swagger": "2.0" -} \ No newline at end of file diff --git a/gae/buildv1openapi.json b/gae/buildv1openapi.json new file mode 100644 index 0000000..a59a137 --- /dev/null +++ b/gae/buildv1openapi.json @@ -0,0 +1,194 @@ +{ + "basePath": "/_ah/api", + "consumes": [ + "application/json" + ], + "definitions": { + "WebappSrcProtoModelBuildInfoMessage": { + "properties": { + "artifact_type": { + "type": "string" + }, + "artifacts": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_id": { + "type": "string" + }, + "build_target": { + "type": "string" + }, + "build_type": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "signed": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelBuildResponseMessage": { + "properties": { + "builds": { + "description": "A message for representing an individual build entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelBuildInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountRequestMessage": { + "properties": { + "filter": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountResponseMessage": { + "properties": { + "count": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDefaultResponse": { + "properties": { + "return_code": { + "enum": [ + "SUCCESS", + "FAIL" + ], + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelGetRequestMessage": { + "properties": { + "direction": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "offset": { + "format": "int64", + "type": "string" + }, + "size": { + "format": "int64", + "type": "string" + }, + "sort": { + "type": "string" + } + }, + "type": "object" + } + }, + "host": "vtslab-schedule-prod.appspot.com", + "info": { + "description": "Endpoint API for build_info.", + "title": "build", + "version": "v1" + }, + "paths": { + "/build/v1/count": { + "post": { + "operationId": "BuildInfoApi_count", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountResponseMessage" + } + } + } + } + }, + "/build/v1/get": { + "post": { + "operationId": "BuildInfoApi_get", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelGetRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelBuildResponseMessage" + } + } + } + } + }, + "/build/v1/set": { + "post": { + "operationId": "BuildInfoApi_set", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelBuildInfoMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" + } + } + } + } + } + }, + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "securityDefinitions": { + "google_id_token": { + "authorizationUrl": "", + "flow": "implicit", + "type": "oauth2", + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" + } + }, + "swagger": "2.0", + "x-google-api-name": "build" +} \ No newline at end of file diff --git a/gae/host_infov1openapi.json b/gae/host_infov1openapi.json deleted file mode 100644 index cadfb6e..0000000 --- a/gae/host_infov1openapi.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "basePath": "/_ah/api", - "consumes": [ - "application/json" - ], - "definitions": { - "WebappSrcProtoModelDefaultResponse": { - "properties": { - "return_code": { - "enum": [ - "SUCCESS", - "FAIL" - ], - "type": "string" - } - }, - "type": "object" - }, - "WebappSrcProtoModelDeviceInfoMessage": { - "properties": { - "product": { - "type": "string" - }, - "scheduling_status": { - "format": "int64", - "type": "string" - }, - "serial": { - "type": "string" - }, - "status": { - "format": "int64", - "type": "string" - } - }, - "type": "object" - }, - "WebappSrcProtoModelHostInfoMessage": { - "properties": { - "devices": { - "description": "A message for representing an individual host's device entry.", - "items": { - "$ref": "#/definitions/WebappSrcProtoModelDeviceInfoMessage" - }, - "type": "array" - }, - "hostname": { - "type": "string" - } - }, - "type": "object" - } - }, - "host": "vtslab-schedule-prod.appspot.com", - "info": { - "description": "Endpoint API for host_info.", - "title": "host_info", - "version": "v1" - }, - "paths": { - "/host_info/v1/set": { - "post": { - "operationId": "HostInfoApi_set", - "parameters": [ - { - "in": "body", - "name": "body", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelHostInfoMessage" - } - } - ], - "responses": { - "200": { - "description": "A successful response", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" - } - } - } - } - } - }, - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "securityDefinitions": { - "google_id_token": { - "authorizationUrl": "", - "flow": "implicit", - "type": "oauth2", - "x-google-issuer": "https://accounts.google.com", - "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" - } - }, - "swagger": "2.0" -} \ No newline at end of file diff --git a/gae/hostv1openapi.json b/gae/hostv1openapi.json new file mode 100644 index 0000000..35116b2 --- /dev/null +++ b/gae/hostv1openapi.json @@ -0,0 +1,243 @@ +{ + "basePath": "/_ah/api", + "consumes": [ + "application/json" + ], + "definitions": { + "WebappSrcProtoModelBuildInfoMessage": { + "properties": { + "artifact_type": { + "type": "string" + }, + "artifacts": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_id": { + "type": "string" + }, + "build_target": { + "type": "string" + }, + "build_type": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "signed": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelBuildResponseMessage": { + "properties": { + "builds": { + "description": "A message for representing an individual build entry.", + "items": { + "$ref": "#/definitions/#/definitions/WebappSrcProtoModelBuildInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountRequestMessage": { + "properties": { + "filter": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountResponseMessage": { + "properties": { + "count": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDefaultResponse": { + "properties": { + "return_code": { + "enum": [ + "SUCCESS", + "FAIL" + ], + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceInfoMessage": { + "properties": { + "product": { + "type": "string" + }, + "scheduling_status": { + "format": "int64", + "type": "string" + }, + "serial": { + "type": "string" + }, + "status": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceResponseMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelGetRequestMessage": { + "properties": { + "direction": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "offset": { + "format": "int64", + "type": "string" + }, + "size": { + "format": "int64", + "type": "string" + }, + "sort": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelHostInfoMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "hostname": { + "type": "string" + } + }, + "type": "object" + } + }, + "host": "vtslab-schedule-prod.appspot.com", + "info": { + "description": "Endpoint API for host_info.", + "title": "host", + "version": "v1" + }, + "paths": { + "/host/v1/count": { + "post": { + "operationId": "HostInfoApi_count", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountResponseMessage" + } + } + } + } + }, + "/host/v1/get": { + "post": { + "operationId": "HostInfoApi_get", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelGetRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDeviceResponseMessage" + } + } + } + } + }, + "/host/v1/set": { + "post": { + "operationId": "HostInfoApi_set", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelHostInfoMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" + } + } + } + } + } + }, + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "securityDefinitions": { + "google_id_token": { + "authorizationUrl": "", + "flow": "implicit", + "type": "oauth2", + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" + } + }, + "swagger": "2.0", + "x-google-api-name": "host" +} \ No newline at end of file diff --git a/gae/jobv1openapi.json b/gae/jobv1openapi.json new file mode 100644 index 0000000..e2badde --- /dev/null +++ b/gae/jobv1openapi.json @@ -0,0 +1,718 @@ +{ + "basePath": "/_ah/api", + "consumes": [ + "application/json" + ], + "definitions": { + "WebappSrcProtoModelBuildInfoMessage": { + "properties": { + "artifact_type": { + "type": "string" + }, + "artifacts": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_id": { + "type": "string" + }, + "build_target": { + "type": "string" + }, + "build_type": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "signed": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelBuildResponseMessage": { + "properties": { + "builds": { + "description": "A message for representing an individual build entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/#/definitions/#/definitions/WebappSrcProtoModelBuildInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountRequestMessage": { + "properties": { + "filter": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountResponseMessage": { + "properties": { + "count": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDefaultResponse": { + "properties": { + "return_code": { + "enum": [ + "SUCCESS", + "FAIL" + ], + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceInfoMessage": { + "properties": { + "product": { + "type": "string" + }, + "scheduling_status": { + "format": "int64", + "type": "string" + }, + "serial": { + "type": "string" + }, + "status": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceResponseMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelGetRequestMessage": { + "properties": { + "direction": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "offset": { + "format": "int64", + "type": "string" + }, + "size": { + "format": "int64", + "type": "string" + }, + "sort": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelHostInfoMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "hostname": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelJobLeaseResponse": { + "properties": { + "jobs": { + "description": "A message for representing an individual job entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelJobMessage" + }, + "type": "array" + }, + "return_code": { + "enum": [ + "SUCCESS", + "FAIL" + ], + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelJobMessage": { + "properties": { + "build_id": { + "type": "string" + }, + "build_storage_type": { + "format": "int64", + "type": "string" + }, + "build_target": { + "type": "string" + }, + "device": { + "type": "string" + }, + "gsi_branch": { + "type": "string" + }, + "gsi_build_id": { + "type": "string" + }, + "gsi_build_target": { + "type": "string" + }, + "gsi_pab_account_id": { + "type": "string" + }, + "gsi_storage_type": { + "format": "int64", + "type": "string" + }, + "gsi_vendor_version": { + "type": "string" + }, + "has_bootloader_img": { + "type": "boolean" + }, + "has_radio_img": { + "type": "boolean" + }, + "hostname": { + "type": "string" + }, + "image_package_repo_base": { + "type": "string" + }, + "infra_log_url": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "pab_account_id": { + "type": "string" + }, + "param": { + "items": { + "type": "string" + }, + "type": "array" + }, + "period": { + "format": "int64", + "type": "string" + }, + "priority": { + "type": "string" + }, + "report_bucket": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_persistent_url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_reference_url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_spreadsheet_id": { + "items": { + "type": "string" + }, + "type": "array" + }, + "require_signed_device_build": { + "type": "boolean" + }, + "retry_count": { + "format": "int64", + "type": "string" + }, + "serial": { + "items": { + "type": "string" + }, + "type": "array" + }, + "shards": { + "format": "int64", + "type": "string" + }, + "status": { + "format": "int64", + "type": "string" + }, + "test_branch": { + "type": "string" + }, + "test_build_id": { + "type": "string" + }, + "test_build_target": { + "type": "string" + }, + "test_name": { + "type": "string" + }, + "test_pab_account_id": { + "type": "string" + }, + "test_storage_type": { + "format": "int64", + "type": "string" + }, + "test_type": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelJobResponseMessage": { + "properties": { + "has_next": { + "type": "boolean" + }, + "jobs": { + "description": "A message for representing an individual job entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelJobMessage" + }, + "type": "array" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabDeviceInfoMessage": { + "properties": { + "device_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "product": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabHostInfoMessage": { + "properties": { + "device": { + "description": "A message for representing an individual lab host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/WebappSrcProtoModelLabDeviceInfoMessage" + }, + "type": "array" + }, + "host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "script": { + "type": "string" + }, + "vtslab_version": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabInfoMessage": { + "properties": { + "admin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "host": { + "description": "A message for representing an individual lab's host entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/WebappSrcProtoModelLabHostInfoMessage" + }, + "type": "array" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabMessage": { + "properties": { + "admin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "devices": { + "type": "string" + }, + "host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "vtslab_version": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabResponseMessage": { + "properties": { + "has_next": { + "type": "boolean" + }, + "labs": { + "description": "A model for representing a LabModel entity.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/WebappSrcProtoModelLabMessage" + }, + "type": "array" + } + }, + "type": "object" + }, + "WebappSrcProtoModelScheduleInfoMessage": { + "properties": { + "build_storage_type": { + "format": "int64", + "type": "string" + }, + "build_target": { + "type": "string" + }, + "device": { + "items": { + "type": "string" + }, + "type": "array" + }, + "device_pab_account_id": { + "type": "string" + }, + "gsi_branch": { + "type": "string" + }, + "gsi_build_target": { + "type": "string" + }, + "gsi_pab_account_id": { + "type": "string" + }, + "gsi_storage_type": { + "format": "int64", + "type": "string" + }, + "gsi_vendor_version": { + "type": "string" + }, + "has_bootloader_img": { + "type": "boolean" + }, + "has_radio_img": { + "type": "boolean" + }, + "image_package_repo_base": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "items": { + "type": "string" + }, + "type": "array" + }, + "param": { + "items": { + "type": "string" + }, + "type": "array" + }, + "period": { + "format": "int64", + "type": "string" + }, + "priority": { + "type": "string" + }, + "report_bucket": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_persistent_url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_reference_url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_spreadsheet_id": { + "items": { + "type": "string" + }, + "type": "array" + }, + "require_signed_device_build": { + "type": "boolean" + }, + "required_device_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "required_host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "retry_count": { + "format": "int64", + "type": "string" + }, + "schedule": { + "type": "string" + }, + "schedule_type": { + "type": "string" + }, + "shards": { + "format": "int64", + "type": "string" + }, + "test_branch": { + "type": "string" + }, + "test_build_target": { + "type": "string" + }, + "test_name": { + "type": "string" + }, + "test_pab_account_id": { + "type": "string" + }, + "test_storage_type": { + "format": "int64", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelScheduleResponseMessage": { + "properties": { + "has_next": { + "type": "boolean" + }, + "schedules": { + "description": "A message for representing an individual schedule entry.", + "items": { + "$ref": "#/definitions/#/definitions/WebappSrcProtoModelScheduleInfoMessage" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "host": "vtslab-schedule-prod.appspot.com", + "info": { + "description": "Endpoint API for job_queue.", + "title": "job", + "version": "v1" + }, + "paths": { + "/job/v1/count": { + "post": { + "operationId": "JobQueueApi_count", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountResponseMessage" + } + } + } + } + }, + "/job/v1/get": { + "post": { + "operationId": "JobQueueApi_get", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelGetRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelJobResponseMessage" + } + } + } + } + }, + "/job/v1/heartbeat": { + "post": { + "operationId": "JobQueueApi_heartbeat", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelJobMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelJobLeaseResponse" + } + } + } + } + }, + "/job/v1/lease": { + "post": { + "operationId": "JobQueueApi_lease", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelJobMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelJobLeaseResponse" + } + } + } + } + } + }, + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "securityDefinitions": { + "google_id_token": { + "authorizationUrl": "", + "flow": "implicit", + "type": "oauth2", + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" + } + }, + "swagger": "2.0", + "x-google-api-name": "job" +} \ No newline at end of file diff --git a/gae/lab_infov1openapi.json b/gae/lab_infov1openapi.json deleted file mode 100644 index 18b6b9c..0000000 --- a/gae/lab_infov1openapi.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "basePath": "/_ah/api", - "consumes": [ - "application/json" - ], - "definitions": { - "WebappSrcProtoModelDefaultResponse": { - "properties": { - "return_code": { - "enum": [ - "SUCCESS", - "FAIL" - ], - "type": "string" - } - }, - "type": "object" - }, - "WebappSrcProtoModelLabHostInfoMessage": { - "properties": { - "hostname": { - "type": "string" - }, - "ip": { - "type": "string" - }, - "script": { - "type": "string" - } - }, - "type": "object" - }, - "WebappSrcProtoModelLabInfoMessage": { - "properties": { - "host": { - "items": { - "$ref": "#/definitions/WebappSrcProtoModelLabHostInfoMessage" - }, - "type": "array" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "string" - } - }, - "type": "object" - } - }, - "host": "vtslab-schedule-prod.appspot.com", - "info": { - "title": "lab_info", - "version": "v1" - }, - "paths": { - "/lab_info/v1/clear": { - "post": { - "operationId": "LabInfoApi_clear", - "parameters": [ - { - "in": "body", - "name": "body", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelLabInfoMessage" - } - } - ], - "responses": { - "200": { - "description": "A successful response", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" - } - } - } - } - }, - "/lab_info/v1/set": { - "post": { - "operationId": "LabInfoApi_set", - "parameters": [ - { - "in": "body", - "name": "body", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelLabInfoMessage" - } - } - ], - "responses": { - "200": { - "description": "A successful response", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" - } - } - } - } - } - }, - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "securityDefinitions": { - "google_id_token": { - "authorizationUrl": "", - "flow": "implicit", - "type": "oauth2", - "x-google-issuer": "https://accounts.google.com", - "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" - } - }, - "swagger": "2.0" -} \ No newline at end of file diff --git a/gae/labv1openapi.json b/gae/labv1openapi.json new file mode 100644 index 0000000..37f31d2 --- /dev/null +++ b/gae/labv1openapi.json @@ -0,0 +1,408 @@ +{ + "basePath": "/_ah/api", + "consumes": [ + "application/json" + ], + "definitions": { + "WebappSrcProtoModelBuildInfoMessage": { + "properties": { + "artifact_type": { + "type": "string" + }, + "artifacts": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_id": { + "type": "string" + }, + "build_target": { + "type": "string" + }, + "build_type": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "signed": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelBuildResponseMessage": { + "properties": { + "builds": { + "description": "A message for representing an individual build entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/WebappSrcProtoModelBuildInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountRequestMessage": { + "properties": { + "filter": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountResponseMessage": { + "properties": { + "count": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDefaultResponse": { + "properties": { + "return_code": { + "enum": [ + "SUCCESS", + "FAIL" + ], + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceInfoMessage": { + "properties": { + "product": { + "type": "string" + }, + "scheduling_status": { + "format": "int64", + "type": "string" + }, + "serial": { + "type": "string" + }, + "status": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceResponseMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelGetRequestMessage": { + "properties": { + "direction": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "offset": { + "format": "int64", + "type": "string" + }, + "size": { + "format": "int64", + "type": "string" + }, + "sort": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelHostInfoMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "hostname": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabDeviceInfoMessage": { + "properties": { + "device_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "product": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabHostInfoMessage": { + "properties": { + "device": { + "description": "A message for representing an individual lab host's device entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelLabDeviceInfoMessage" + }, + "type": "array" + }, + "host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "script": { + "type": "string" + }, + "vtslab_version": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabInfoMessage": { + "properties": { + "admin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "host": { + "description": "A message for representing an individual lab's host entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelLabHostInfoMessage" + }, + "type": "array" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabMessage": { + "properties": { + "admin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "devices": { + "type": "string" + }, + "host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "vtslab_version": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabResponseMessage": { + "properties": { + "has_next": { + "type": "boolean" + }, + "labs": { + "description": "A model for representing a LabModel entity.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelLabMessage" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "host": "vtslab-schedule-prod.appspot.com", + "info": { + "description": "Endpoint API for lab_info.", + "title": "lab", + "version": "v1" + }, + "paths": { + "/lab/v1/clear": { + "post": { + "operationId": "LabInfoApi_clear", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelLabInfoMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" + } + } + } + } + }, + "/lab/v1/count": { + "post": { + "operationId": "LabInfoApi_count", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountResponseMessage" + } + } + } + } + }, + "/lab/v1/get": { + "post": { + "operationId": "LabInfoApi_get", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelGetRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelLabResponseMessage" + } + } + } + } + }, + "/lab/v1/set": { + "post": { + "operationId": "LabInfoApi_set", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelLabInfoMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" + } + } + } + } + }, + "/lab/v1/set_version": { + "post": { + "operationId": "LabInfoApi_setVersion", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelLabHostInfoMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" + } + } + } + } + } + }, + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "securityDefinitions": { + "google_id_token": { + "authorizationUrl": "", + "flow": "implicit", + "type": "oauth2", + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" + } + }, + "swagger": "2.0", + "x-google-api-name": "lab" +} \ No newline at end of file diff --git a/gae/schedule_infov1openapi.json b/gae/schedule_infov1openapi.json deleted file mode 100644 index d03cc4c..0000000 --- a/gae/schedule_infov1openapi.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "basePath": "/_ah/api", - "consumes": [ - "application/json" - ], - "definitions": { - "WebappSrcProtoModelDefaultResponse": { - "properties": { - "return_code": { - "enum": [ - "SUCCESS", - "FAIL" - ], - "type": "string" - } - }, - "type": "object" - }, - "WebappSrcProtoModelScheduleInfoMessage": { - "properties": { - "build_target": { - "type": "string" - }, - "device": { - "items": { - "type": "string" - }, - "type": "array" - }, - "gsi_branch": { - "type": "string" - }, - "gsi_build_target": { - "type": "string" - }, - "gsi_pab_account_id": { - "type": "string" - }, - "manifest_branch": { - "type": "string" - }, - "param": { - "items": { - "type": "string" - }, - "type": "array" - }, - "period": { - "format": "int64", - "type": "string" - }, - "priority": { - "type": "string" - }, - "shards": { - "format": "int64", - "type": "string" - }, - "retry_count": { - "format": "int64", - "type": "string" - }, - "test_name": { - "type": "string" - }, - "test_branch": { - "type": "string" - }, - "test_build_target": { - "type": "string" - }, - "test_pab_account_id": { - "type": "string" - } - }, - "type": "object" - } - }, - "host": "vtslab-schedule-prod.appspot.com", - "info": { - "description": "Endpoint API for schedule_info.", - "title": "schedule_info", - "version": "v1" - }, - "paths": { - "/schedule_info/v1/clear": { - "post": { - "operationId": "ScheduleInfoApi_clear", - "parameters": [ - { - "in": "body", - "name": "body", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelScheduleInfoMessage" - } - } - ], - "responses": { - "200": { - "description": "A successful response", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" - } - } - } - } - }, - "/schedule_info/v1/set": { - "post": { - "operationId": "ScheduleInfoApi_set", - "parameters": [ - { - "in": "body", - "name": "body", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelScheduleInfoMessage" - } - } - ], - "responses": { - "200": { - "description": "A successful response", - "schema": { - "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" - } - } - } - } - } - }, - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "securityDefinitions": { - "google_id_token": { - "authorizationUrl": "", - "flow": "implicit", - "type": "oauth2", - "x-google-issuer": "https://accounts.google.com", - "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" - } - }, - "swagger": "2.0" -} \ No newline at end of file diff --git a/gae/schedulev1openapi.json b/gae/schedulev1openapi.json new file mode 100644 index 0000000..b1db4e9 --- /dev/null +++ b/gae/schedulev1openapi.json @@ -0,0 +1,545 @@ +{ + "basePath": "/_ah/api", + "consumes": [ + "application/json" + ], + "definitions": { + "WebappSrcProtoModelBuildInfoMessage": { + "properties": { + "artifact_type": { + "type": "string" + }, + "artifacts": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_id": { + "type": "string" + }, + "build_target": { + "type": "string" + }, + "build_type": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "signed": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelBuildResponseMessage": { + "properties": { + "builds": { + "description": "A message for representing an individual build entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/#/definitions/WebappSrcProtoModelBuildInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountRequestMessage": { + "properties": { + "filter": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelCountResponseMessage": { + "properties": { + "count": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDefaultResponse": { + "properties": { + "return_code": { + "enum": [ + "SUCCESS", + "FAIL" + ], + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceInfoMessage": { + "properties": { + "product": { + "type": "string" + }, + "scheduling_status": { + "format": "int64", + "type": "string" + }, + "serial": { + "type": "string" + }, + "status": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelDeviceResponseMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "has_next": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebappSrcProtoModelGetRequestMessage": { + "properties": { + "direction": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "offset": { + "format": "int64", + "type": "string" + }, + "size": { + "format": "int64", + "type": "string" + }, + "sort": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelHostInfoMessage": { + "properties": { + "devices": { + "description": "A message for representing an individual host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/#/definitions/WebappSrcProtoModelDeviceInfoMessage" + }, + "type": "array" + }, + "hostname": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabDeviceInfoMessage": { + "properties": { + "device_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "product": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabHostInfoMessage": { + "properties": { + "device": { + "description": "A message for representing an individual lab host's device entry.", + "items": { + "$ref": "#/definitions/#/definitions/WebappSrcProtoModelLabDeviceInfoMessage" + }, + "type": "array" + }, + "host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "script": { + "type": "string" + }, + "vtslab_version": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabInfoMessage": { + "properties": { + "admin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "host": { + "description": "A message for representing an individual lab's host entry.", + "items": { + "$ref": "#/definitions/#/definitions/WebappSrcProtoModelLabHostInfoMessage" + }, + "type": "array" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabMessage": { + "properties": { + "admin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "devices": { + "type": "string" + }, + "host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "vtslab_version": { + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelLabResponseMessage": { + "properties": { + "has_next": { + "type": "boolean" + }, + "labs": { + "description": "A model for representing a LabModel entity.", + "items": { + "$ref": "#/definitions/#/definitions/WebappSrcProtoModelLabMessage" + }, + "type": "array" + } + }, + "type": "object" + }, + "WebappSrcProtoModelScheduleInfoMessage": { + "properties": { + "build_storage_type": { + "format": "int64", + "type": "string" + }, + "build_target": { + "type": "string" + }, + "device": { + "items": { + "type": "string" + }, + "type": "array" + }, + "device_pab_account_id": { + "type": "string" + }, + "gsi_branch": { + "type": "string" + }, + "gsi_build_target": { + "type": "string" + }, + "gsi_pab_account_id": { + "type": "string" + }, + "gsi_storage_type": { + "format": "int64", + "type": "string" + }, + "gsi_vendor_version": { + "type": "string" + }, + "has_bootloader_img": { + "type": "boolean" + }, + "has_radio_img": { + "type": "boolean" + }, + "image_package_repo_base": { + "type": "string" + }, + "manifest_branch": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "items": { + "type": "string" + }, + "type": "array" + }, + "param": { + "items": { + "type": "string" + }, + "type": "array" + }, + "period": { + "format": "int64", + "type": "string" + }, + "priority": { + "type": "string" + }, + "report_bucket": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_persistent_url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_reference_url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "report_spreadsheet_id": { + "items": { + "type": "string" + }, + "type": "array" + }, + "require_signed_device_build": { + "type": "boolean" + }, + "required_device_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "required_host_equipment": { + "items": { + "type": "string" + }, + "type": "array" + }, + "retry_count": { + "format": "int64", + "type": "string" + }, + "schedule": { + "type": "string" + }, + "schedule_type": { + "type": "string" + }, + "shards": { + "format": "int64", + "type": "string" + }, + "test_branch": { + "type": "string" + }, + "test_build_target": { + "type": "string" + }, + "test_name": { + "type": "string" + }, + "test_pab_account_id": { + "type": "string" + }, + "test_storage_type": { + "format": "int64", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, + "WebappSrcProtoModelScheduleResponseMessage": { + "properties": { + "has_next": { + "type": "boolean" + }, + "schedules": { + "description": "A message for representing an individual schedule entry.", + "items": { + "$ref": "#/definitions/WebappSrcProtoModelScheduleInfoMessage" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "host": "vtslab-schedule-prod.appspot.com", + "info": { + "description": "Endpoint API for schedule_info.", + "title": "schedule", + "version": "v1" + }, + "paths": { + "/schedule/v1/clear": { + "post": { + "operationId": "ScheduleInfoApi_clear", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelScheduleInfoMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" + } + } + } + } + }, + "/schedule/v1/count": { + "post": { + "operationId": "ScheduleInfoApi_count", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelCountResponseMessage" + } + } + } + } + }, + "/schedule/v1/get": { + "post": { + "operationId": "ScheduleInfoApi_get", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelGetRequestMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelScheduleResponseMessage" + } + } + } + } + }, + "/schedule/v1/set": { + "post": { + "operationId": "ScheduleInfoApi_set", + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelScheduleInfoMessage" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/WebappSrcProtoModelDefaultResponse" + } + } + } + } + } + }, + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "securityDefinitions": { + "google_id_token": { + "authorizationUrl": "", + "flow": "implicit", + "type": "oauth2", + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" + } + }, + "swagger": "2.0", + "x-google-api-name": "schedule" +} \ No newline at end of file -- cgit v1.2.3 From 37563ae9902dd4e3e64ef9912057bacf2a431cc8 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 6 Sep 2018 17:05:12 +0900 Subject: Sort job page by timestamp. Test: dev_appserver.py app.yaml && ng serve Bug: 74575555 --- gae/frontend/src/app/menu/job/job.component.html | 6 +++--- gae/frontend/src/app/menu/job/job.component.ts | 11 +++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index d8dbcca..b9c68c1 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -13,8 +13,8 @@ limitations under the License. -->
-
- + + No. {{i+1+pageSize*pageIndex}} @@ -124,7 +124,7 @@ -
+ (); pageEvent: PageEvent; + sort = ''; + sortDirection = ''; + constructor(private jobService: JobService) { super(); } ngOnInit(): void { + // By default, job page requires list in desc order by timestamp. + this.sort = 'timestamp'; + this.sortDirection = 'desc'; + this.getCount(); this.getJobs(this.pageSize, this.pageSize * this.pageIndex); } @@ -73,7 +80,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { getJobs(size = 0, offset = 0) { this.loading = true; const filterJSON = ''; - this.jobService.getJobs(size, offset, filterJSON, '', '') + this.jobService.getJobs(size, offset, filterJSON, this.sort, this.sortDirection) .subscribe( (response) => { this.loading = false; -- cgit v1.2.3 From 0f4a2420cdda7a1e9da352522eb267bdb620913a Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 5 Sep 2018 19:03:00 +0900 Subject: Add statistics table in job page. Test: go/vtslab-schedule-dev Bug: 74575555 Change-Id: I8156f1e77608ee032c834e9c466392b2f36d2b89 --- gae/frontend/src/app/menu/job/job.component.html | 41 +++++++++++++++++++ gae/frontend/src/app/menu/job/job.component.ts | 50 ++++++++++++++++++++++++ gae/frontend/src/app/model/filter_condition.ts | 24 ++++++++++++ gae/frontend/src/app/model/filter_item.ts | 22 +++++++++++ gae/frontend/src/app/shared/vtslab_status.ts | 23 +++++++++++ gae/frontend/src/styles.scss | 8 ++++ gae/webapp/src/endpoint/endpoint_base.py | 14 +++++++ gae/webapp/src/proto/model.py | 4 +- 8 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 gae/frontend/src/app/model/filter_condition.ts create mode 100644 gae/frontend/src/app/model/filter_item.ts create mode 100644 gae/frontend/src/app/shared/vtslab_status.ts diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index d8dbcca..3628464 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -12,6 +12,47 @@ See the License for the specific language governing permissions and limitations under the License. --> +
+ + + Stats + {{stat.hours}} + + + + Created + {{stat.created}} + + + + Completed + {{stat.completed}} ({{stat.created > 0 ? stat.completed/stat.created*100 : 0}})% + + + + Running/Ready + {{stat.running}} ({{stat.created > 0 ? stat.running/stat.created*100 : 0}})% + + + + Boot-up Error + {{stat.bootup_err}} ({{stat.created > 0 ? stat.bootup_err/stat.created*100 : 0}})% + + + + Infra Error + {{stat.infra_err}} ({{stat.created > 0 ? stat.infra_err/stat.created*100 : 0}})% + + + + Expired + {{stat.expired}} ({{stat.created > 0 ? stat.expired/stat.created*100 : 0}})% + + + + + +
diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index d3caccf..c1a31bb 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -16,9 +16,14 @@ import { Component, OnInit } from '@angular/core'; import { MatTableDataSource, PageEvent } from '@angular/material'; +import { FilterCondition } from '../../model/filter_condition'; +import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; import { Job } from '../../model/job'; import { JobService } from './job.service'; +import { JobStatus } from '../../shared/vtslab_status'; + +import * as moment from 'moment-timezone'; /** Component that handles job menu. */ @Component({ @@ -48,7 +53,17 @@ export class JobComponent extends MenuBaseClass implements OnInit { 'test_type', 'timestamp', ]; + statColumnTitles = [ + 'hours', + 'created', + 'completed', + 'running', + 'bootup_err', + 'infra_err', + 'expired', + ]; dataSource = new MatTableDataSource(); + statDataSource = new MatTableDataSource(); pageEvent: PageEvent; constructor(private jobService: JobService) { @@ -57,6 +72,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { ngOnInit(): void { this.getCount(); + this.getStatistics(); this.getJobs(this.pageSize, this.pageSize * this.pageIndex); } @@ -118,4 +134,38 @@ export class JobComponent extends MenuBaseClass implements OnInit { this.getJobs(this.pageSize, this.pageSize * this.pageIndex); return event; } + + /** Gets the recent jobs and calculate statistics */ + getStatistics() { + const timeFilter = new FilterItem(); + timeFilter.key = 'timestamp'; + timeFilter.method = FilterCondition.GreaterThan; + timeFilter.value = '72'; + const timeFilterString = JSON.stringify([timeFilter]); + this.jobService.getJobs(0, 0, timeFilterString, '', '') + .subscribe( + (response) => { + const stats_72hrs = this.buildStatisticsData('72 Hours', response.jobs); + const jobs_24hrs = response.jobs.filter( + job => (moment() - moment.tz(job.timestamp, 'YYYY-MM-DDThh:mm:ss', 'UTC')) / 3600000 < 24); + const stats_24hrs = this.buildStatisticsData('24 Hours', jobs_24hrs); + this.statDataSource.data = [stats_24hrs, stats_72hrs]; + }, + (error) => console.log(`[${error.status}] ${error.name}`) + ); + } + + /** Builds statistics from given jobs list */ + buildStatisticsData(title, jobs) { + return { + hours: title, + created: jobs.length, + completed: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.complete).length, + running: jobs.filter(job => job.status != null && + (Number(job.status) === JobStatus.leased || Number(job.status) === JobStatus.ready)).length, + bootup_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.bootup_err).length, + infra_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.infra_err).length, + expired: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.expired).length, + }; + } } diff --git a/gae/frontend/src/app/model/filter_condition.ts b/gae/frontend/src/app/model/filter_condition.ts new file mode 100644 index 0000000..9f76de9 --- /dev/null +++ b/gae/frontend/src/app/model/filter_condition.ts @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export enum FilterCondition { + EqualTo = 1, + LessThan, + GreaterThan, + LessThanOrEqualTo, + GreaterThanOrEqualTo, + NotEqualTo, + Has, +} diff --git a/gae/frontend/src/app/model/filter_item.ts b/gae/frontend/src/app/model/filter_item.ts new file mode 100644 index 0000000..de457a1 --- /dev/null +++ b/gae/frontend/src/app/model/filter_item.ts @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {FilterCondition} from './filter_condition'; + +export class FilterItem { + key: string; + method: FilterCondition; + value: string; // back-end should handle type-casting. +} diff --git a/gae/frontend/src/app/shared/vtslab_status.ts b/gae/frontend/src/app/shared/vtslab_status.ts new file mode 100644 index 0000000..5f063ed --- /dev/null +++ b/gae/frontend/src/app/shared/vtslab_status.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class JobStatus { + static readonly ready = 0; + static readonly leased = 1; + static readonly complete = 2; + static readonly infra_err = 3; + static readonly expired = 4; + static readonly bootup_err = 5; +} diff --git a/gae/frontend/src/styles.scss b/gae/frontend/src/styles.scss index aabb7b0..61e8933 100644 --- a/gae/frontend/src/styles.scss +++ b/gae/frontend/src/styles.scss @@ -14,6 +14,14 @@ body { } } +.statistics-table { + margin: 10px 20px 20px 20px; + + table { + width: 100%; + } +} + .entity-table { margin: 10px 20px 20px 20px; diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py index acf13ae..0e429dd 100644 --- a/gae/webapp/src/endpoint/endpoint_base.py +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import inspect import logging import json @@ -203,6 +204,19 @@ class EndpointBase(remote.Service): else: logging.debug("Empty repeated list cannot be queried.") empty_repeated_field.append(value) + elif isinstance(metaclass._properties[property_key], + ndb.DateTimeProperty): + if method == Status.FILTER_METHOD[Status.FILTER_LessThan]: + query = query.filter( + getattr(metaclass, property_key) < datetime.datetime. + now() - datetime.timedelta(hours=int(value))) + elif method == Status.FILTER_METHOD[Status.FILTER_GreaterThan]: + query = query.filter( + getattr(metaclass, property_key) > datetime.datetime. + now() - datetime.timedelta(hours=int(value))) + else: + logging.debug("DateTimeProperty only allows <=(less than) " + "and >=(greater than) operation.") else: if method == Status.FILTER_METHOD[Status.FILTER_EqualTo]: query = query.filter( diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 25af6dc..352ee56 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -297,7 +297,7 @@ class JobModel(ndb.Model): class JobMessage(messages.Message): """A message for representing an individual job entry.""" - # Next ID = 35 + # Next ID = 38 test_type = messages.IntegerField(29) hostname = messages.StringField(1) @@ -347,6 +347,8 @@ class JobMessage(messages.Message): report_persistent_url = messages.StringField(35, repeated=True) report_reference_url = messages.StringField(36, repeated=True) + timestamp = message_types.DateTimeField(37) + class ReturnCodeMessage(messages.Enum): """Enum for default return code.""" -- cgit v1.2.3 From f315416b99390d805e85e96b40cad8e695499ec3 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 10 Sep 2018 16:14:37 +0900 Subject: Display Enum's name instead of value. Test: ng serve Bug: 74575555 --- .../src/app/menu/device/device.component.html | 4 +- .../src/app/menu/device/device.component.ts | 3 ++ gae/frontend/src/app/menu/job/job.component.html | 4 +- gae/frontend/src/app/menu/job/job.component.ts | 27 +++++++++--- gae/frontend/src/app/shared/vtslab_status.ts | 49 ++++++++++++++++++---- gae/frontend/tslint.json | 2 +- 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/gae/frontend/src/app/menu/device/device.component.html b/gae/frontend/src/app/menu/device/device.component.html index cce6cf2..0c32f39 100644 --- a/gae/frontend/src/app/menu/device/device.component.html +++ b/gae/frontend/src/app/menu/device/device.component.html @@ -41,13 +41,13 @@ Status - {{device.status}} + {{deviceStatusEnum[device.status]}} Scheduling Status - {{device.scheduling_status}} + {{schedulingStatusEnum[device.scheduling_status]}} diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index cec9f76..d462af2 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -18,6 +18,7 @@ import { MatTableDataSource, PageEvent } from '@angular/material'; import { Device } from '../../model/device'; import { DeviceService } from './device.service'; +import { DeviceStatus, SchedulingStatus } from '../../shared/vtslab_status'; import { MenuBaseClass } from '../menu_base'; /** Component that handles device menu. */ @@ -39,6 +40,8 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { ]; dataSource = new MatTableDataSource(); pageEvent: PageEvent; + deviceStatusEnum = DeviceStatus; + schedulingStatusEnum = SchedulingStatus; constructor(private deviceService: DeviceService) { super(); diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 3628464..9429c69 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -64,7 +64,7 @@ Test Type - {{job.test_type}} + {{getTestTypeText(job.test_type)}} @@ -148,7 +148,7 @@ Status - {{job.status}} + {{jobStatusEnum[job.status]}} diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index c1a31bb..a299b14 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -21,7 +21,7 @@ import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; import { Job } from '../../model/job'; import { JobService } from './job.service'; -import { JobStatus } from '../../shared/vtslab_status'; +import { JobStatus, TestType } from '../../shared/vtslab_status'; import * as moment from 'moment-timezone'; @@ -65,6 +65,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { dataSource = new MatTableDataSource(); statDataSource = new MatTableDataSource(); pageEvent: PageEvent; + jobStatusEnum = JobStatus; constructor(private jobService: JobService) { super(); @@ -160,12 +161,26 @@ export class JobComponent extends MenuBaseClass implements OnInit { return { hours: title, created: jobs.length, - completed: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.complete).length, + completed: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Complete).length, running: jobs.filter(job => job.status != null && - (Number(job.status) === JobStatus.leased || Number(job.status) === JobStatus.ready)).length, - bootup_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.bootup_err).length, - infra_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.infra_err).length, - expired: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.expired).length, + (Number(job.status) === JobStatus.Leased || Number(job.status) === JobStatus.Ready)).length, + bootup_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Bootup_err).length, + infra_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Infra_err).length, + expired: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Expired).length, }; } + + /** Generates text to represent in HTML with given test type. */ + getTestTypeText(status: number) { + if (status === undefined || status & TestType.Unknown) { + return TestType[TestType.Unknown]; + } + + const text_list = []; + [TestType.ToT, TestType.OTA, TestType.Signed, TestType.Manual].forEach(function (value) { + if (status & value) { text_list.push(TestType[value]); } + }); + + return text_list.join(', '); + } } diff --git a/gae/frontend/src/app/shared/vtslab_status.ts b/gae/frontend/src/app/shared/vtslab_status.ts index 5f063ed..2836f97 100644 --- a/gae/frontend/src/app/shared/vtslab_status.ts +++ b/gae/frontend/src/app/shared/vtslab_status.ts @@ -13,11 +13,46 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export class JobStatus { - static readonly ready = 0; - static readonly leased = 1; - static readonly complete = 2; - static readonly infra_err = 3; - static readonly expired = 4; - static readonly bootup_err = 5; +export enum JobStatus { + Ready = 0, + Leased, + Complete, + Infra_err, + Expired, + Bootup_err, +} + +export enum DeviceStatus { + Unknown = 0, + Fastboot, + Online, + Ready, + Use, + Error, + No_response, +} + +export enum SchedulingStatus { + Free = 0, + Reserved, + Use, +} + +/** + * bit 0-1 : version related test type + * 00 - Unknown + * 01 - ToT + * 10 - OTA + * bit 2 : device signed build + * bit 3-4 : reserved for gerrit related test type + * 01 - pre-submit + * bit 5 : manually created test job + */ +export enum TestType { + Unknown = 0, + ToT = 1, + OTA = 1 << 1, + Signed = 1 << 2, + Presubmit = 1 << 3, + Manual = 1 << 5, } diff --git a/gae/frontend/tslint.json b/gae/frontend/tslint.json index 3ea984c..259320f 100644 --- a/gae/frontend/tslint.json +++ b/gae/frontend/tslint.json @@ -44,7 +44,7 @@ } ], "no-arg": true, - "no-bitwise": true, + "no-bitwise": false, "no-console": [ true, "debug", -- cgit v1.2.3 From fcc9a3f1bed4357fca5d5816502d153f251b38b2 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 10 Sep 2018 13:44:15 +0900 Subject: Check whether jobs array is empty. Test: ng serve Bug: 74575555 Change-Id: I92fc5d15c3640a68ac6be20d2f4cea7f40669d2c --- gae/frontend/src/app/menu/job/job.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index a299b14..3ea3240 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -147,7 +147,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { .subscribe( (response) => { const stats_72hrs = this.buildStatisticsData('72 Hours', response.jobs); - const jobs_24hrs = response.jobs.filter( + const jobs_24hrs = (response.jobs == null || response.jobs.length === 0) ? undefined : response.jobs.filter( job => (moment() - moment.tz(job.timestamp, 'YYYY-MM-DDThh:mm:ss', 'UTC')) / 3600000 < 24); const stats_24hrs = this.buildStatisticsData('24 Hours', jobs_24hrs); this.statDataSource.data = [stats_24hrs, stats_72hrs]; @@ -158,6 +158,9 @@ export class JobComponent extends MenuBaseClass implements OnInit { /** Builds statistics from given jobs list */ buildStatisticsData(title, jobs) { + if (jobs == null || jobs.length === 0) { + return { hours: title, created: 0, completed: 0, running: 0, bootup_err: 0, infra_err: 0, expired: 0 }; + } return { hours: title, created: jobs.length, -- cgit v1.2.3 From 5772ef433b66655fd7a48926e42f490f6d43c7b5 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 11 Sep 2018 15:39:17 +0900 Subject: Display only two digits after decimal point. Test: ng serve Bug: 74575555 --- gae/frontend/src/app/menu/job/job.component.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 9429c69..4405607 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -26,27 +26,27 @@ Completed - {{stat.completed}} ({{stat.created > 0 ? stat.completed/stat.created*100 : 0}})% + {{stat.completed}} ({{stat.created > 0 ? (stat.completed/stat.created*100 | number:'1.0-2') : 0}})% Running/Ready - {{stat.running}} ({{stat.created > 0 ? stat.running/stat.created*100 : 0}})% + {{stat.running}} ({{stat.created > 0 ? (stat.running/stat.created*100 | number:'1.0-2') : 0}})% Boot-up Error - {{stat.bootup_err}} ({{stat.created > 0 ? stat.bootup_err/stat.created*100 : 0}})% + {{stat.bootup_err}} ({{stat.created > 0 ? (stat.bootup_err/stat.created*100 | number:'1.0-2') : 0}})% Infra Error - {{stat.infra_err}} ({{stat.created > 0 ? stat.infra_err/stat.created*100 : 0}})% + {{stat.infra_err}} ({{stat.created > 0 ? (stat.infra_err/stat.created*100 | number:'1.0-2') : 0}})% Expired - {{stat.expired}} ({{stat.created > 0 ? stat.expired/stat.created*100 : 0}})% + {{stat.expired}} ({{stat.created > 0 ? (stat.expired/stat.created*100 | number:'1.0-2') : 0}})% -- cgit v1.2.3 From 94696902d2bd801d0a96451d1d500a5b5543048c Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 11 Sep 2018 10:15:33 +0900 Subject: Update deploy code to include frontend codes. Test: ./script/deploy-webapp.sh dev Bug: 74575555 Change-Id: I439eee0f4db738cfdffa1ee8897f601fbb153200 --- gae/app.yaml | 11 ++++++----- gae/frontend/angular.json | 2 +- gae/script/deploy-webapp.sh | 33 +++++++++++++++++++++++++++++++++ gae/webapp/src/webapp_main.py | 9 --------- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/gae/app.yaml b/gae/app.yaml index c2b8dfe..374c07c 100644 --- a/gae/app.yaml +++ b/gae/app.yaml @@ -19,12 +19,13 @@ handlers: - url: /_ah/api/.* script: webapp.src.endpoint_main.api -- url: /favicon\.ico - static_files: favicon.ico - upload: favicon\.ico +- url: /(.*\.(html|js|css|txt|ico)) + static_files: webapp/static/\1 + upload: webapp/static/(.*\.(html|js|css|txt|ico)) -- url: /bootstrap - static_dir: webapp/static/bootstrap +- url: /((build|device|job|lab|schedule)([?&/].*)?)? + static_files: webapp/static/index.html + upload: webapp/static/index.html - url: /.* script: webapp.src.webapp_main.app diff --git a/gae/frontend/angular.json b/gae/frontend/angular.json index e1c4e1a..ca84a43 100644 --- a/gae/frontend/angular.json +++ b/gae/frontend/angular.json @@ -13,7 +13,7 @@ "build": { "builder": "@angular-devkit/build-angular:browser", "options": { - "outputPath": "dist/frontend", + "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", diff --git a/gae/script/deploy-webapp.sh b/gae/script/deploy-webapp.sh index f3cc91e..aeea2a0 100755 --- a/gae/script/deploy-webapp.sh +++ b/gae/script/deploy-webapp.sh @@ -19,6 +19,39 @@ if [ "$#" -ne 1 ]; then exit 1 fi +NPM_PATH=$(which npm) +NG_PATH=$(which ng) +if [ ! -f "${NPM_PATH}" ]; then + echo "Cannot find npm in your PATH." + echo "Please install node.js and npm to deploy frontend." + exit 0 +fi +if [ ! -f "${NG_PATH}" ]; then + echo "Cannot find Angular CLI in your PATH." + echo "Please install Angular CLI to deploy frontend." + exit 0 +fi + +pushd frontend +echo "Installing frontend dependencies..." +npm install + +echo "Removing files in dist directory..." +rm -r dist/* + +echo "Building frontend codes..." +if [ $1 = "local" ]; then + ng build +else + ng build --prod +fi +popd + +echo "Copying frontend files to webapp/static directory..." +rm -rf webapp/static/ +mkdir webapp/static +cp -r frontend/dist/* webapp/static/ + if [ $1 = "public" ]; then SERVICE="vtslab-schedule" elif [ $1 = "local" ]; then diff --git a/gae/webapp/src/webapp_main.py b/gae/webapp/src/webapp_main.py index 380db83..f5c256d 100644 --- a/gae/webapp/src/webapp_main.py +++ b/gae/webapp/src/webapp_main.py @@ -19,10 +19,6 @@ import os import webapp2 -from webapp.src.dashboard import build_list -from webapp.src.dashboard import device_list -from webapp.src.dashboard import job_list -from webapp.src.dashboard import schedule_list from webapp.src.handlers import base from webapp.src.scheduler import device_heartbeat from webapp.src.scheduler import job_heartbeat @@ -49,11 +45,6 @@ config['webapp2_extras.sessions'] = { app = webapp2.WSGIApplication( [ - ("/", MainPage), ("/build", build_list.BuildPage), - ("/device", device_list.DevicePage), ("/job", job_list.JobPage), - ("/create_job", job_list.CreateJobPage), - ("/create_job_template", job_list.CreateJobTemplatePage), - ("/result", MainPage), ("/schedule", schedule_list.SchedulePage), ("/tasks/schedule", periodic.PeriodicScheduler), ("/tasks/device_heartbeat", device_heartbeat.PeriodicDeviceHeartBeat), ("/tasks/job_heartbeat", job_heartbeat.PeriodicJobHeartBeat), -- cgit v1.2.3 From 448bd10f3ccfb5b9a7071653a48efa87099afadb Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 12 Sep 2018 15:53:24 +0900 Subject: Update a script do deploy endpoints. Now it supports dev server as well, and a bug which did not display a list of endpoints has been fixed. Test: ./script/deploy-endpoints.sh dev Bug: 113777904 --- gae/script/deploy-endpoint.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gae/script/deploy-endpoint.sh b/gae/script/deploy-endpoint.sh index 8c4e1fd..a6cf10b 100755 --- a/gae/script/deploy-endpoint.sh +++ b/gae/script/deploy-endpoint.sh @@ -19,12 +19,10 @@ if [ "$#" -ne 1 ]; then exit 1 fi -if [ $1 = "prod" ]; then - SERVICE="vtslab-schedule-prod" -elif [ $1 = "public" ]; then +if [ $1 = "public" ]; then SERVICE="vtslab-schedule" else - SERVICE="vtslab-schedule-test" + SERVICE="vtslab-schedule-$1" fi echo "Creating OpenAPI spec files for $SERVICE.appspot.com ..." @@ -33,6 +31,6 @@ python lib/endpoints/endpointscfg.py get_openapi_spec webapp.src.endpoint.build_ echo "Depolying the endpoint API implementation to $SERVICE ..." gcloud endpoints services deploy buildv1openapi.json hostv1openapi.json labv1openapi.json schedulev1openapi.json jobv1openapi.json --project=$SERVICE -gcloud endpoints configs list --service=$SERVICE +gcloud endpoints configs list --service=$SERVICE.appspot.com echo "Deployment done!" -- cgit v1.2.3 From bc0f6a04a33967477e7b48a51674d29f7db4c17d Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 13 Sep 2018 13:44:47 +0900 Subject: Create a script to pack vtslab scheduler. Test: ./script/pack-gae.sh Bug: 115589691 --- script/pack-gae.sh | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100755 script/pack-gae.sh diff --git a/script/pack-gae.sh b/script/pack-gae.sh new file mode 100755 index 0000000..9917aef --- /dev/null +++ b/script/pack-gae.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Copyright 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -d "$ANDROID_BUILD_TOP" ]; then + TEST_SERVING_DIR="$ANDROID_BUILD_TOP/test/vti/test_serving" +else + CURRENT_DIR_NAME="${PWD##*/}" + if [ "${CURRENT_DIR_NAME}" = "test_serving" ]; then + TEST_SERVING_DIR="${PWD}" + elif [ "${CURRENT_DIR_NAME}" = "script" ]; then + TEST_SERVING_DIR="${PWD}/.." + else + echo "Missing ANDROID_BUILD_TOP env variable. Run 'lunch' first." + exit 1 + fi +fi + +if [ ! -d "${TEST_SERVING_DIR}/gae" ]; then + echo "Please run this script in 'test_serving' directory." + exit 1 +fi + +pushd $TEST_SERVING_DIR/gae +echo "Removing unnecessary files in ${TEST_SERVING_DIR}/gae directory..." +git clean -f + +echo "Updating python libraries..." +rm -rf lib/ +./script/install-pip.sh +popd + +pushd $TEST_SERVING_DIR/ +zip vtslab-scheduler-$(git log -s -n 1 --format="%cd" --date=format:"%Y%m%d_%H%M%S")-$(git rev-parse --short HEAD).zip -r gae -x *.pyc "*/\.*" *.DS_Store* gae/frontend/node_modules**\* +popd -- cgit v1.2.3 From 55f048d55cda750281acb49c449f6ab03db5a761 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 13 Sep 2018 16:49:55 +0900 Subject: Display relative time. Frontend has datetime as YYYY-MM-DDThh:mm:ss string format, so it needs to be translated to human-friendly format. Another time format would be good, for example, 2018-09-20 12:34:56, however the most practices users want to see relative time. The heartbeats for devices and jobs are good examples. Show details button is going to be introduced soon and in that UI users still can see the raw time value. Test: go/vtslab-schedule-dev Bug: 74575555 Change-Id: I996ac5ea23912a8e5ee3b842ec147fa3df319a47 --- gae/frontend/src/app/menu/job/job.component.html | 4 ++-- gae/frontend/src/app/menu/menu_base.ts | 8 ++++++++ gae/frontend/src/app/menu/schedule/schedule.component.html | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 6b7550a..7ba1c89 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -154,13 +154,13 @@ Timestamp - {{job.timestamp}} + {{getRelativeTime(job.timestamp)}} Heartbeat - {{job.heartbeat_stamp}} + {{getRelativeTime(job.heartbeat_stamp)}} diff --git a/gae/frontend/src/app/menu/menu_base.ts b/gae/frontend/src/app/menu/menu_base.ts index f34bad2..ab120ee 100644 --- a/gae/frontend/src/app/menu/menu_base.ts +++ b/gae/frontend/src/app/menu/menu_base.ts @@ -17,6 +17,9 @@ /** This class defines and/or implements the common properties and methods * used among menus. */ +import moment from 'moment-timezone'; + + export abstract class MenuBaseClass { count = -1; @@ -42,4 +45,9 @@ export abstract class MenuBaseClass { error: (error) => console.log(`[${error.status}] ${error.name}`) }; } + + getRelativeTime(timeString) { + return (moment.tz(timeString, 'YYYY-MM-DDThh:mm:ss', 'UTC').isValid() ? + moment.tz(timeString, 'YYYY-MM-DDThh:mm:ss', 'UTC').fromNow() : timeString); + } } diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html index f453b23..a061b79 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.html +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -77,7 +77,7 @@ Timestamp - {{schedule.timestamp}} + {{getRelativeTime(schedule.timestamp)}} -- cgit v1.2.3 From 0fd5dd495ebbebaa8f116d50e5424ad725673fc0 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Fri, 14 Sep 2018 13:58:52 +0900 Subject: Order table columns and add missing property. Test: ng serve Bug: 74575555 --- gae/frontend/src/app/menu/build/build.component.ts | 2 +- .../src/app/menu/device/device.component.ts | 4 ++-- gae/frontend/src/app/menu/job/job.component.ts | 22 +++++++++++----------- gae/frontend/src/app/menu/lab/lab.component.html | 12 ++++++------ gae/frontend/src/app/menu/lab/lab.component.ts | 8 ++++---- .../src/app/menu/schedule/schedule.component.ts | 8 ++++---- gae/webapp/src/proto/model.py | 3 ++- 7 files changed, 30 insertions(+), 29 deletions(-) diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index 8ebe22a..a40d78d 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -31,10 +31,10 @@ export class BuildComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', 'artifact_type', + 'manifest_branch', 'build_id', 'build_target', 'build_type', - 'manifest_branch', 'signed']; dataSource = new MatTableDataSource(); pageEvent: PageEvent; diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index d462af2..412eee2 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -31,12 +31,12 @@ import { MenuBaseClass } from '../menu_base'; export class DeviceComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', - 'device_equipment', 'hostname', 'product', - 'scheduling_status', 'serial', 'status', + 'scheduling_status', + 'device_equipment', ]; dataSource = new MatTableDataSource(); pageEvent: PageEvent; diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index ae4fbcb..035ac05 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -35,23 +35,23 @@ import * as moment from 'moment-timezone'; export class JobComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', - 'build_id', - 'build_target', + 'test_type', + 'test_name', + 'hostname', 'device', + 'serial', + 'manifest_branch', + 'build_target', + 'build_id', 'gsi_branch', - 'gsi_build_id', 'gsi_build_target', - 'heartbeat_stamp', - 'hostname', - 'manifest_branch', - 'serial', - 'status', + 'gsi_build_id', 'test_branch', - 'test_build_id', 'test_build_target', - 'test_name', - 'test_type', + 'test_build_id', + 'status', 'timestamp', + 'heartbeat_stamp', ]; statColumnTitles = [ 'hours', diff --git a/gae/frontend/src/app/menu/lab/lab.component.html b/gae/frontend/src/app/menu/lab/lab.component.html index 5459168..0392e2d 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.html +++ b/gae/frontend/src/app/menu/lab/lab.component.html @@ -82,18 +82,18 @@ {{host.ip}} - - - Version - {{host.vtslab_version}} - - Equipment {{host.host_equipment}} + + + Version + {{host.vtslab_version}} + + diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts index db9dc09..38992d2 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.ts +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -31,17 +31,17 @@ import { MenuBaseClass } from '../menu_base'; export class LabComponent extends MenuBaseClass implements OnInit { labColumnTitles = [ '_index', - 'admin', - 'hostCount', 'name', 'owner', + 'admin', + 'hostCount', ]; hostColumnTitles = [ '_index', - 'host_equipment', + 'name', 'hostname', 'ip', - 'name', + 'host_equipment', 'vtslab_version', ]; labCount = -1; diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index 87305fe..ff320c3 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -30,15 +30,15 @@ import { ScheduleService } from './schedule.service'; export class ScheduleComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', - 'build_target', + 'test_name', 'device', + 'manifest_branch', + 'build_target', 'gsi_branch', 'gsi_build_target', - 'manifest_branch', - 'period', 'test_branch', 'test_build_target', - 'test_name', + 'period', 'timestamp', ]; dataSource = new MatTableDataSource(); diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 352ee56..52b7a8a 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -297,7 +297,7 @@ class JobModel(ndb.Model): class JobMessage(messages.Message): """A message for representing an individual job entry.""" - # Next ID = 38 + # Next ID = 39 test_type = messages.IntegerField(29) hostname = messages.StringField(1) @@ -348,6 +348,7 @@ class JobMessage(messages.Message): report_reference_url = messages.StringField(36, repeated=True) timestamp = message_types.DateTimeField(37) + heartbeat_stamp = message_types.DateTimeField(38) class ReturnCodeMessage(messages.Enum): -- cgit v1.2.3 From 107c108f32edd38dcfc996a997f4c476cdd95fa9 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 20 Sep 2018 14:17:22 +0900 Subject: Redirect to specific appspot version. Test: go/vtslab-schedule-dev/redirect/20180911t150917-dot-vtslab-schedule-dev Bug: 116179456 Change-Id: I744cee9924f4f171c50058aa4e25aead97d64255 --- gae/webapp/src/webapp_main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gae/webapp/src/webapp_main.py b/gae/webapp/src/webapp_main.py index f5c256d..eaaa5ee 100644 --- a/gae/webapp/src/webapp_main.py +++ b/gae/webapp/src/webapp_main.py @@ -26,6 +26,13 @@ from webapp.src.scheduler import periodic from webapp.src.tasks import indexing +class RedirectHandler(base.BaseHandler): + """Redirect handler to redirect to specific appspot version.""" + def get(self, arg): + if arg: + return self.redirect("https://{}.appspot.com/".format(arg)) + + class MainPage(base.BaseHandler): """Main web page request handler.""" @@ -49,6 +56,7 @@ app = webapp2.WSGIApplication( ("/tasks/device_heartbeat", device_heartbeat.PeriodicDeviceHeartBeat), ("/tasks/job_heartbeat", job_heartbeat.PeriodicJobHeartBeat), ("/tasks/indexing([/]?.*)", indexing.CreateIndex), + ("/redirect/(.*)", RedirectHandler), ], config=config, debug=False) -- cgit v1.2.3 From 8595d40e4aa98bfa32020613d3fc8cd14ba4ec4c Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 19 Sep 2018 19:30:41 +0900 Subject: Add snackbar UI to display notifications. Test: go/vtslab-schedule-dev/redirect/20180920t150955-dot-vtslab-schedule-dev Bug: 74575555 Change-Id: Id4d0b249c4eaf6fc95224bd112a571bae6400903 --- gae/frontend/src/app/app.module.ts | 3 +++ gae/frontend/src/app/menu/build/build.component.ts | 11 ++++++----- gae/frontend/src/app/menu/dashboard/dashboard.component.ts | 4 ++++ gae/frontend/src/app/menu/device/device.component.ts | 11 ++++++----- gae/frontend/src/app/menu/job/job.component.ts | 13 +++++++------ gae/frontend/src/app/menu/lab/lab.component.ts | 9 +++++---- gae/frontend/src/app/menu/menu_base.ts | 12 ++++++++++-- gae/frontend/src/app/menu/schedule/schedule.component.ts | 11 ++++++----- gae/frontend/src/app/shared/servicebase.ts | 3 +-- 9 files changed, 48 insertions(+), 29 deletions(-) diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index e3e8a1c..f383cf5 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -23,6 +23,7 @@ import { RouterModule, Routes } from '@angular/router'; // Angular Material modules import { MatPaginatorModule } from '@angular/material/paginator'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -54,12 +55,14 @@ const appRoutes: Routes = [ imports: [ MatPaginatorModule, MatProgressSpinnerModule, + MatSnackBarModule, MatTableModule, MatTabsModule, ], exports: [ MatPaginatorModule, MatProgressSpinnerModule, + MatSnackBarModule, MatTableModule, MatTabsModule, ] diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index a40d78d..c584f80 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Component, OnInit } from '@angular/core'; -import { MatTableDataSource, PageEvent } from '@angular/material'; +import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { Build } from '../../model/build'; import { BuildService } from './build.service'; @@ -39,8 +39,9 @@ export class BuildComponent extends MenuBaseClass implements OnInit { dataSource = new MatTableDataSource(); pageEvent: PageEvent; - constructor(private buildService: BuildService) { - super(); + constructor(private buildService: BuildService, + public snackBar: MatSnackBar) { + super(snackBar); } ngOnInit(): void { @@ -73,7 +74,7 @@ export class BuildComponent extends MenuBaseClass implements OnInit { const total = length + offset; if (response.has_next) { if (length !== this.pageSize) { - console.log('Received unexpected number of entities.'); + this.showSnackbar('Received unexpected number of entities.'); } else if (this.count <= total) { this.getCount(); } @@ -95,7 +96,7 @@ export class BuildComponent extends MenuBaseClass implements OnInit { } this.dataSource.data = response.builds; }, - (error) => console.log(`[${error.status}] ${error.name}`) + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts index 8094303..57ea988 100644 --- a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { Component } from '@angular/core'; +import { MatSnackBar } from '@angular/material'; /** Component that handles dashboard. */ @Component({ @@ -22,4 +23,7 @@ import { Component } from '@angular/core'; styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent { + constructor(public snackBar: MatSnackBar) { + this.snackBar.dismiss(); + } } diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index 412eee2..890b84c 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Component, OnInit } from '@angular/core'; -import { MatTableDataSource, PageEvent } from '@angular/material'; +import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { Device } from '../../model/device'; import { DeviceService } from './device.service'; @@ -43,8 +43,9 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { deviceStatusEnum = DeviceStatus; schedulingStatusEnum = SchedulingStatus; - constructor(private deviceService: DeviceService) { - super(); + constructor(private deviceService: DeviceService, + public snackBar: MatSnackBar) { + super(snackBar); } ngOnInit(): void { @@ -77,7 +78,7 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { const total = length + offset; if (response.has_next) { if (length !== this.pageSize) { - console.log('Received unexpected number of entities.'); + this.showSnackbar('Received unexpected number of entities.'); } else if (this.count <= total) { this.getCount(); } @@ -99,7 +100,7 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { } this.dataSource.data = response.devices; }, - (error) => console.log(`[${error.status}] ${error.name}`) + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index 035ac05..901ff03 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Component, OnInit } from '@angular/core'; -import { MatTableDataSource, PageEvent, Sort } from '@angular/material'; +import { MatSnackBar, MatTableDataSource, PageEvent, Sort } from '@angular/material'; import { FilterCondition } from '../../model/filter_condition'; import { FilterItem } from '../../model/filter_item'; @@ -70,8 +70,9 @@ export class JobComponent extends MenuBaseClass implements OnInit { sort = ''; sortDirection = ''; - constructor(private jobService: JobService) { - super(); + constructor(private jobService: JobService, + public snackBar: MatSnackBar) { + super(snackBar); } ngOnInit(): void { @@ -109,7 +110,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { const total = length + offset; if (response.has_next) { if (length !== this.pageSize) { - console.log('Received unexpected number of entities.'); + this.showSnackbar('Received unexpected number of entities.'); } else if (this.count <= total) { this.getCount(); } @@ -131,7 +132,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { } this.dataSource.data = response.jobs; }, - (error) => console.log(`[${error.status}] ${error.name}`) + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } @@ -159,7 +160,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { const stats_24hrs = this.buildStatisticsData('24 Hours', jobs_24hrs); this.statDataSource.data = [stats_24hrs, stats_72hrs]; }, - (error) => console.log(`[${error.status}] ${error.name}`) + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts index 38992d2..83fe3a1 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.ts +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Component, OnInit } from '@angular/core'; -import {MatTableDataSource, PageEvent} from '@angular/material'; +import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { Host } from '../../model/host'; import { Lab } from '../../model/lab'; @@ -47,8 +47,9 @@ export class LabComponent extends MenuBaseClass implements OnInit { labCount = -1; labPageIndex = 0; - constructor(private labService: LabService) { - super(); + constructor(private labService: LabService, + public snackBar: MatSnackBar) { + super(snackBar); } labDataSource = new MatTableDataSource(); @@ -77,7 +78,7 @@ export class LabComponent extends MenuBaseClass implements OnInit { this.setLabs(response.labs); } }, - (error) => console.log(`[${error.status}] ${error.name}`) + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } diff --git a/gae/frontend/src/app/menu/menu_base.ts b/gae/frontend/src/app/menu/menu_base.ts index ab120ee..8d568b0 100644 --- a/gae/frontend/src/app/menu/menu_base.ts +++ b/gae/frontend/src/app/menu/menu_base.ts @@ -17,6 +17,7 @@ /** This class defines and/or implements the common properties and methods * used among menus. */ +import { MatSnackBar } from '@angular/material'; import moment from 'moment-timezone'; @@ -28,7 +29,8 @@ export abstract class MenuBaseClass { pageSize = 100; pageIndex = 0; - protected constructor() { + protected constructor(public snackBar: MatSnackBar) { + this.snackBar.dismiss(); } /** Returns an Observable which handles a response of count API. @@ -42,7 +44,7 @@ export abstract class MenuBaseClass { operation(response); } }, - error: (error) => console.log(`[${error.status}] ${error.name}`) + error: (error) => this.showSnackbar(`[${error.status}] ${error.name}`) }; } @@ -50,4 +52,10 @@ export abstract class MenuBaseClass { return (moment.tz(timeString, 'YYYY-MM-DDThh:mm:ss', 'UTC').isValid() ? moment.tz(timeString, 'YYYY-MM-DDThh:mm:ss', 'UTC').fromNow() : timeString); } + + /** Displays a snackbar notification. */ + showSnackbar(message = 'Error', duration = 5000) { + this.loading = false; + this.snackBar.open(message, 'DISMISS', {duration}); + } } diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index ff320c3..23f79d3 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Component, OnInit } from '@angular/core'; -import { MatTableDataSource, PageEvent } from '@angular/material'; +import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { MenuBaseClass } from '../menu_base'; import { Schedule } from '../../model/schedule'; @@ -44,8 +44,9 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { dataSource = new MatTableDataSource(); pageEvent: PageEvent; - constructor(private scheduleService: ScheduleService) { - super(); + constructor(private scheduleService: ScheduleService, + public snackBar: MatSnackBar) { + super(snackBar); } ngOnInit(): void { @@ -78,7 +79,7 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { const total = length + offset; if (response.has_next) { if (length !== this.pageSize) { - console.log('Received unexpected number of entities.'); + this.showSnackbar('Received unexpected number of entities.'); } else if (this.count <= total) { this.getCount(); } @@ -100,7 +101,7 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { } this.dataSource.data = response.schedules; }, - (error) => console.log(`[${error.status}] ${error.name}`) + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } diff --git a/gae/frontend/src/app/shared/servicebase.ts b/gae/frontend/src/app/shared/servicebase.ts index 94a4f67..9eaecf8 100644 --- a/gae/frontend/src/app/shared/servicebase.ts +++ b/gae/frontend/src/app/shared/servicebase.ts @@ -32,8 +32,7 @@ export class ServiceBase { `body was: ${error.error}`); } // return an observable with a user-facing error message - return throwError( - 'Something bad happened; please try again later.'); + return throwError(error); } public getCount(filterInfo: string): Observable { const url = this.url + 'count'; -- cgit v1.2.3 From 5441e551c691348e21cc1aef363232c738336a1f Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 20 Sep 2018 15:24:50 +0900 Subject: Update a deploy script to use deploy options. Test: ./script/deploy-webapp.sh dev --no-promote Bug: 116179456 --- gae/script/deploy-webapp.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gae/script/deploy-webapp.sh b/gae/script/deploy-webapp.sh index aeea2a0..187ee7d 100755 --- a/gae/script/deploy-webapp.sh +++ b/gae/script/deploy-webapp.sh @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [ "$#" -ne 1 ]; then - echo "usage: deploy-webapp.sh prod|test|public|local" +if [ "$#" -lt 1 ]; then + echo "usage: deploy-webapp.sh prod|test|public|local [deploy options]" exit 1 fi @@ -86,6 +86,6 @@ fi echo "Deploying the web app to $SERVICE ..." -gcloud app deploy app.yaml cron.yaml index.yaml queue.yaml worker.yaml --project=$SERVICE +gcloud app deploy app.yaml cron.yaml index.yaml queue.yaml worker.yaml --project=$SERVICE ${@:2} echo "Deployment done!" -- cgit v1.2.3 From 8df76061d197a830ec8f058e67d244b72a43ca3d Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 11 Sep 2018 17:11:41 +0900 Subject: Apply filters. Test: go/vtslab-schedule-dev/redirect/20180920t153040-dot-vtslab-schedule-dev Bug: 74575555 Change-Id: Id20396ce96c1b936616992cd13c88d515d1926a7 --- gae/frontend/src/app/app.module.ts | 29 ++++++ .../src/app/menu/build/build.component.html | 3 + gae/frontend/src/app/menu/build/build.component.ts | 21 +++- .../src/app/menu/device/device.component.html | 3 + .../src/app/menu/device/device.component.ts | 21 +++- gae/frontend/src/app/menu/job/job.component.html | 3 + gae/frontend/src/app/menu/job/job.component.ts | 22 ++++- .../src/app/menu/schedule/schedule.component.html | 3 + .../src/app/menu/schedule/schedule.component.ts | 21 +++- .../src/app/shared/filter/filter.component.html | 66 +++++++++++++ .../src/app/shared/filter/filter.component.scss | 35 +++++++ .../src/app/shared/filter/filter.component.ts | 107 +++++++++++++++++++++ 12 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 gae/frontend/src/app/shared/filter/filter.component.html create mode 100644 gae/frontend/src/app/shared/filter/filter.component.scss create mode 100644 gae/frontend/src/app/shared/filter/filter.component.ts diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index f383cf5..2a60df1 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -16,14 +16,22 @@ // Angular modules. import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; // Angular Material modules +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material'; +import { MatInputModule } from '@angular/material/input'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -32,6 +40,7 @@ import { AppComponent } from './app.component'; import { BuildComponent } from './menu/build/build.component'; import { DashboardComponent } from './menu/dashboard/dashboard.component'; import { DeviceComponent } from './menu/device/device.component'; +import { FilterComponent } from './shared/filter/filter.component'; import { JobComponent } from './menu/job/job.component'; import { LabComponent } from './menu/lab/lab.component'; import { ScheduleComponent } from './menu/schedule/schedule.component'; @@ -39,6 +48,9 @@ import { ScheduleComponent } from './menu/schedule/schedule.component'; // User modules. import { NavModule } from './shared/navbar/navbar'; +// Other dependencies. +import { FlexLayoutModule } from '@angular/flex-layout'; + const appRoutes: Routes = [ { path: 'device', component: DeviceComponent }, @@ -53,16 +65,30 @@ const appRoutes: Routes = [ @NgModule({ imports: [ + MatButtonModule, + MatChipsModule, + MatExpansionModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, MatPaginatorModule, MatProgressSpinnerModule, MatSnackBarModule, + MatSelectModule, MatTableModule, MatTabsModule, ], exports: [ + MatButtonModule, + MatChipsModule, + MatExpansionModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, MatPaginatorModule, MatProgressSpinnerModule, MatSnackBarModule, + MatSelectModule, MatTableModule, MatTabsModule, ] @@ -76,6 +102,7 @@ export class MaterialModule {} BuildComponent, DashboardComponent, DeviceComponent, + FilterComponent, JobComponent, LabComponent, ScheduleComponent, @@ -83,6 +110,8 @@ export class MaterialModule {} imports: [ BrowserAnimationsModule, BrowserModule, + FlexLayoutModule, + FormsModule, HttpClientModule, MaterialModule, NavModule, diff --git a/gae/frontend/src/app/menu/build/build.component.html b/gae/frontend/src/app/menu/build/build.component.html index e2b731b..d8ae525 100644 --- a/gae/frontend/src/app/menu/build/build.component.html +++ b/gae/frontend/src/app/menu/build/build.component.html @@ -12,6 +12,9 @@ See the License for the specific language governing permissions and limitations under the License. --> +
+ +
diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index c584f80..e149f87 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { Build } from '../../model/build'; import { BuildService } from './build.service'; +import { FilterComponent } from '../../shared/filter/filter.component'; +import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; + /** Component that handles build menu. */ @Component({ selector: 'app-build', @@ -38,6 +41,9 @@ export class BuildComponent extends MenuBaseClass implements OnInit { 'signed']; dataSource = new MatTableDataSource(); pageEvent: PageEvent; + appliedFilters: FilterItem[]; + + @ViewChild(FilterComponent) filterComponent: FilterComponent; constructor(private buildService: BuildService, public snackBar: MatSnackBar) { @@ -45,13 +51,14 @@ export class BuildComponent extends MenuBaseClass implements OnInit { } ngOnInit(): void { + this.filterComponent.setSelectorList(Build); this.getCount(); this.getBuilds(this.pageSize, this.pageSize * this.pageIndex); } /** Gets a total count of builds. */ getCount(observer = this.getDefaultCountObservable()) { - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.buildService.getCount(filterJSON).subscribe(observer); } @@ -61,7 +68,7 @@ export class BuildComponent extends MenuBaseClass implements OnInit { */ getBuilds(size = 0, offset = 0) { this.loading = true; - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.buildService.getBuilds(size, offset, filterJSON, '', '') .subscribe( (response) => { @@ -107,4 +114,12 @@ export class BuildComponent extends MenuBaseClass implements OnInit { this.getBuilds(this.pageSize, this.pageSize * this.pageIndex); return event; } + + /** Applies a filter and get entities with it. */ + applyFilters(filters) { + this.pageIndex = 0; + this.appliedFilters = filters; + this.getCount(); + this.getBuilds(this.pageSize, this.pageSize * this.pageIndex); + } } diff --git a/gae/frontend/src/app/menu/device/device.component.html b/gae/frontend/src/app/menu/device/device.component.html index 0c32f39..762e198 100644 --- a/gae/frontend/src/app/menu/device/device.component.html +++ b/gae/frontend/src/app/menu/device/device.component.html @@ -12,6 +12,9 @@ See the License for the specific language governing permissions and limitations under the License. --> +
+ +
diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index 890b84c..1c95fda 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { Device } from '../../model/device'; import { DeviceService } from './device.service'; import { DeviceStatus, SchedulingStatus } from '../../shared/vtslab_status'; +import { FilterComponent } from '../../shared/filter/filter.component'; +import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; + /** Component that handles device menu. */ @Component({ selector: 'app-device', @@ -42,6 +45,9 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { pageEvent: PageEvent; deviceStatusEnum = DeviceStatus; schedulingStatusEnum = SchedulingStatus; + appliedFilters: FilterItem[]; + + @ViewChild(FilterComponent) filterComponent: FilterComponent; constructor(private deviceService: DeviceService, public snackBar: MatSnackBar) { @@ -49,13 +55,14 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { } ngOnInit(): void { + this.filterComponent.setSelectorList(Device); this.getCount(); this.getDevices(this.pageSize, this.pageSize * this.pageIndex); } /** Gets a total count of devices. */ getCount(observer = this.getDefaultCountObservable()) { - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.deviceService.getCount(filterJSON).subscribe(observer); } @@ -65,7 +72,7 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { */ getDevices(size = 0, offset = 0) { this.loading = true; - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.deviceService.getDevices(size, offset, filterJSON, '', '') .subscribe( (response) => { @@ -111,4 +118,12 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { this.getDevices(this.pageSize, this.pageSize * this.pageIndex); return event; } + + /** Applies a filter and get entities with it. */ + applyFilters(filters) { + this.pageIndex = 0; + this.appliedFilters = filters; + this.getCount(); + this.getDevices(this.pageSize, this.pageSize * this.pageIndex); + } } diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 7ba1c89..e938c8a 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -53,6 +53,9 @@ +
+ +
diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index 901ff03..33670d4 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, OnInit } from '@angular/core'; -import { MatSnackBar, MatTableDataSource, PageEvent, Sort } from '@angular/material'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; +import { FilterComponent } from '../../shared/filter/filter.component'; import { FilterCondition } from '../../model/filter_condition'; import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; @@ -25,6 +26,7 @@ import { JobStatus, TestType } from '../../shared/vtslab_status'; import * as moment from 'moment-timezone'; + /** Component that handles job menu. */ @Component({ selector: 'app-job', @@ -66,6 +68,9 @@ export class JobComponent extends MenuBaseClass implements OnInit { statDataSource = new MatTableDataSource(); pageEvent: PageEvent; jobStatusEnum = JobStatus; + appliedFilters: FilterItem[]; + + @ViewChild(FilterComponent) filterComponent: FilterComponent; sort = ''; sortDirection = ''; @@ -80,6 +85,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { this.sort = 'timestamp'; this.sortDirection = 'desc'; + this.filterComponent.setSelectorList(Job); this.getCount(); this.getStatistics(); this.getJobs(this.pageSize, this.pageSize * this.pageIndex); @@ -87,7 +93,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { /** Gets a total count of jobs. */ getCount(observer = this.getDefaultCountObservable()) { - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.jobService.getCount(filterJSON).subscribe(observer); } @@ -97,7 +103,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { */ getJobs(size = 0, offset = 0) { this.loading = true; - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.jobService.getJobs(size, offset, filterJSON, this.sort, this.sortDirection) .subscribe( (response) => { @@ -194,4 +200,12 @@ export class JobComponent extends MenuBaseClass implements OnInit { return text_list.join(', '); } + + /** Applies a filter and get entities with it. */ + applyFilters(filters) { + this.pageIndex = 0; + this.appliedFilters = filters; + this.getCount(); + this.getJobs(this.pageSize, this.pageSize * this.pageIndex); + } } diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html index a061b79..67088fc 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.html +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -12,6 +12,9 @@ See the License for the specific language governing permissions and limitations under the License. --> +
+ +
diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index 23f79d3..61f98fd 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; +import { FilterComponent } from '../../shared/filter/filter.component'; +import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; import { Schedule } from '../../model/schedule'; import { ScheduleService } from './schedule.service'; + /** Component that handles schedule menu. */ @Component({ selector: 'app-schedule', @@ -43,6 +46,9 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { ]; dataSource = new MatTableDataSource(); pageEvent: PageEvent; + appliedFilters: FilterItem[]; + + @ViewChild(FilterComponent) filterComponent: FilterComponent; constructor(private scheduleService: ScheduleService, public snackBar: MatSnackBar) { @@ -50,13 +56,14 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { } ngOnInit(): void { + this.filterComponent.setSelectorList(Schedule); this.getCount(); this.getSchedules(this.pageSize, this.pageSize * this.pageIndex); } /** Gets a total count of schedules. */ getCount(observer = this.getDefaultCountObservable()) { - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.scheduleService.getCount(filterJSON).subscribe(observer); } @@ -66,7 +73,7 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { */ getSchedules(size = 0, offset = 0) { this.loading = true; - const filterJSON = ''; + const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.scheduleService.getSchedules(size, offset, filterJSON, '', '') .subscribe( (response) => { @@ -112,4 +119,12 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { this.getSchedules(this.pageSize, this.pageSize * this.pageIndex); return event; } + + /** Applies a filter and get entities with it. */ + applyFilters(filters) { + this.pageIndex = 0; + this.appliedFilters = filters; + this.getCount(); + this.getSchedules(this.pageSize, this.pageSize * this.pageIndex); + } } diff --git a/gae/frontend/src/app/shared/filter/filter.component.html b/gae/frontend/src/app/shared/filter/filter.component.html new file mode 100644 index 0000000..7381359 --- /dev/null +++ b/gae/frontend/src/app/shared/filter/filter.component.html @@ -0,0 +1,66 @@ + +
+ + + + Filter + + + {{ panelOpenState ? "" : appliedFilters.length + " filters are applied." }} + + + + + + {{ key }} + + + + + + + {{ method.text }} + + + + + + + + + + + {{ filter.key }} {{ getSign(filter) }} {{ filter.value }} + cancel + + +
+ + + +
+
+
diff --git a/gae/frontend/src/app/shared/filter/filter.component.scss b/gae/frontend/src/app/shared/filter/filter.component.scss new file mode 100644 index 0000000..88ae569 --- /dev/null +++ b/gae/frontend/src/app/shared/filter/filter.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#filter-wrapper { + position: relative; + + mat-form-field { + margin-right: 20px; + } +} + +#row_buttons { + float: right; +} + +.mat-stroked-button { + min-width: 80px; + min-height: 15px; + padding-left: 15px; + padding-right: 15px; + font-size: 12px; + margin-right: 15px; +} diff --git a/gae/frontend/src/app/shared/filter/filter.component.ts b/gae/frontend/src/app/shared/filter/filter.component.ts new file mode 100644 index 0000000..0ce66fe --- /dev/null +++ b/gae/frontend/src/app/shared/filter/filter.component.ts @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FilterItem } from '../../model/filter_item'; +import { FilterCondition } from '../../model/filter_condition'; + +@Component({ + selector: 'app-filter', + templateUrl: './filter.component.html', + styleUrls: ['./filter.component.scss'] +}) +export class FilterComponent implements OnInit { + currentFilter: FilterItem; + applyingFilters: FilterItem[] = []; + applyingFilterChanged = false; + appliedFilters: FilterItem[] = []; + selectorList: string[]; + + filterMethods = [ + {value: FilterCondition.EqualTo, text: 'is equal to', sign: '='}, + {value: FilterCondition.LessThan, text: 'is less than', sign: '<'}, + {value: FilterCondition.GreaterThan, text: 'is greater than', sign: '>'}, + {value: FilterCondition.LessThanOrEqualTo, text: 'is less than or equal to', sign: '<='}, + {value: FilterCondition.GreaterThanOrEqualTo, text: 'is greater than or equal to', sign: '>='}, + {value: FilterCondition.NotEqualTo, text: 'is not equal to', sign: '!='}, + {value: FilterCondition.Has, text: 'has', sign: 'has'}, + ]; + + @Output() applyFilters = new EventEmitter(); + @Input() disabled: boolean; + + panelOpenState = false; + + ngOnInit(): void { + this.currentFilter = new FilterItem(); + this.currentFilter.value = ''; + } + + /** Sets a filter key list with the given class. */ + setSelectorList(typeOfClass: any) { + const instance = new typeOfClass(); + this.selectorList = Object.getOwnPropertyNames(instance); + } + + /** Adds the current filter to the list of filters to be applied. */ + addFilter() { + this.applyingFilters.push(this.currentFilter); + this.currentFilter = new FilterItem(); + this.currentFilter.value = ''; + this.applyingFilterChanged = true; + } + + /** Clears the current filter. */ + clearCurrentFilter() { + this.currentFilter.key = undefined; + this.currentFilter.method = undefined; + this.currentFilter.value = ''; + } + + /** Removes the selected filter from the list of filters to be applied. */ + removed(filter: FilterItem) { + const index = this.applyingFilters.indexOf(filter); + if (index >= 0) { + this.applyingFilters.splice(index, 1); + this.applyingFilterChanged = true; + } + } + + /** Gets a filter sign with method value. */ + getSign(filter: FilterItem) { + return this.filterMethods.find((x) => x.value === filter.method).sign; + } + + /** Applies the list of filters. */ + onApplyClicked() { + this.applyFilters.emit(this.applyingFilters); + this.appliedFilters = this.applyingFilters.slice(); + this.applyingFilterChanged = false; + } + + /** Cancels the current changes and roll back to the last applied filters. */ + onCancelChangesClicked() { + this.applyingFilters = this.appliedFilters.slice(); + this.applyingFilterChanged = false; + } + + /** Reset all filters. */ + onClearAllClicked() { + this.applyingFilters = []; + this.appliedFilters = []; + this.applyFilters.emit(this.appliedFilters); + this.applyingFilterChanged = false; + } +} -- cgit v1.2.3 From 086619542c2b7bb3b5879f6ac1c7bcf15aa0f819 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 13 Sep 2018 15:04:36 +0900 Subject: Extend rows to have extended features. Test: go/vtslab-schedule-dev/redirect/20180921t132238-dot-vtslab-schedule-dev Bug: 74575555 Change-Id: I6acc2ae820d6b3c5d464efbdd43ec4f14e5db04c --- gae/frontend/src/app/app.module.ts | 4 ++ .../src/app/menu/cdk-detail-row.directive.ts | 72 ++++++++++++++++++++++ gae/frontend/src/app/menu/job/job.component.html | 11 +++- gae/frontend/src/app/menu/job/job.component.ts | 8 +++ gae/frontend/src/styles.scss | 4 ++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 gae/frontend/src/app/menu/cdk-detail-row.directive.ts diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index 2a60df1..cacff6e 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -51,6 +51,9 @@ import { NavModule } from './shared/navbar/navbar'; // Other dependencies. import { FlexLayoutModule } from '@angular/flex-layout'; +// User directives for CDK (Component Development Kit). +import { CdkDetailRowDirective } from './menu/cdk-detail-row.directive'; + const appRoutes: Routes = [ { path: 'device', component: DeviceComponent }, @@ -100,6 +103,7 @@ export class MaterialModule {} declarations: [ AppComponent, BuildComponent, + CdkDetailRowDirective, DashboardComponent, DeviceComponent, FilterComponent, diff --git a/gae/frontend/src/app/menu/cdk-detail-row.directive.ts b/gae/frontend/src/app/menu/cdk-detail-row.directive.ts new file mode 100644 index 0000000..60b490a --- /dev/null +++ b/gae/frontend/src/app/menu/cdk-detail-row.directive.ts @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Directive, EventEmitter, HostBinding, HostListener, Input, Output, TemplateRef, ViewContainerRef} from '@angular/core'; + + +@Directive({ + selector: '[appCdkDetailRow]' +}) +export class CdkDetailRowDirective { + private row: any; + private tRef: TemplateRef; + private opened: boolean; + + @HostBinding('class.expanded') + get expended(): boolean { + return this.opened; + } + + @Input() + set appCdkDetailRow(value: any) { + if (value !== this.row) { + this.row = value; + // this.render(); + } + } + + @Input('appCdkDetailRowTpl') + set template(value: TemplateRef) { + if (value !== this.tRef) { + this.tRef = value; + } + } + + @Output() toggleChange = new EventEmitter(); + + constructor(public vcRef: ViewContainerRef) { } + + @HostListener('click') + onClick(): void { + this.toggle(); + } + + toggle(): void { + if (this.opened) { + this.vcRef.clear(); + } else { + this.render(); + } + this.opened = this.vcRef.length > 0; + this.toggleChange.emit(this); + } + + private render(): void { + this.vcRef.clear(); + if (this.tRef && this.row) { + this.vcRef.createEmbeddedView(this.tRef, { $implicit: this.row }); + } + } +} diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index e938c8a..62ce0f6 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -167,9 +167,11 @@ - +
-
+ + +
diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index 33670d4..383ae19 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -15,6 +15,7 @@ */ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; +import { animate, state, style, transition, trigger } from '@angular/animations'; import { FilterComponent } from '../../shared/filter/filter.component'; import { FilterCondition } from '../../model/filter_condition'; @@ -33,6 +34,13 @@ import * as moment from 'moment-timezone'; templateUrl: './job.component.html', providers: [ JobService ], styleUrls: ['./job.component.scss'], + animations: [ + trigger('detailExpand', [ + state('void', style({height: '0px', minHeight: '0', visibility: 'hidden'})), + state('*', style({height: '*', visibility: 'visible'})), + transition('void <=> *', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + ]), + ], }) export class JobComponent extends MenuBaseClass implements OnInit { columnTitles = [ diff --git a/gae/frontend/src/styles.scss b/gae/frontend/src/styles.scss index 61e8933..0c95e64 100644 --- a/gae/frontend/src/styles.scss +++ b/gae/frontend/src/styles.scss @@ -40,3 +40,7 @@ body { right: 0; position: fixed; } + +.div-expandable { + padding: 10px; +} -- cgit v1.2.3 From 4c2607629d3de9c03005710496a12cd84a7467d0 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Thu, 27 Sep 2018 06:04:10 +0900 Subject: Add a schedule suspend feature in extended rows. Test: go/vtslab-schedule-dev/redirect/20180927t073313-dot-vtslab-schedule-dev Bug: 116058216 --- .../src/app/menu/schedule/schedule.component.html | 12 ++++++++- .../src/app/menu/schedule/schedule.component.ts | 29 ++++++++++++++++++++- .../src/app/menu/schedule/schedule.service.ts | 7 +++++ gae/frontend/src/app/model/schedule.ts | 12 +++++++++ gae/webapp/src/endpoint/endpoint_base.py | 2 ++ gae/webapp/src/endpoint/schedule_info.py | 30 ++++++++++++++++++++++ gae/webapp/src/proto/model.py | 16 +++++++++++- 7 files changed, 105 insertions(+), 3 deletions(-) diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html index 67088fc..c567639 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.html +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -84,7 +84,10 @@ - +
+ +
+ +
+
diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index 61f98fd..57ebd7b 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -15,11 +15,12 @@ */ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; +import { animate, state, style, transition, trigger } from "@angular/animations"; import { FilterComponent } from '../../shared/filter/filter.component'; import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; -import { Schedule } from '../../model/schedule'; +import { Schedule, ScheduleSuspendResponse } from '../../model/schedule'; import { ScheduleService } from './schedule.service'; @@ -29,6 +30,13 @@ import { ScheduleService } from './schedule.service'; templateUrl: './schedule.component.html', providers: [ ScheduleService ], styleUrls: ['./schedule.component.scss'], + animations: [ + trigger('detailExpand', [ + state('void', style({height: '0px', minHeight: '0', visibility: 'hidden'})), + state('*', style({height: '*', visibility: 'visible'})), + transition('void <=> *', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + ]), + ], }) export class ScheduleComponent extends MenuBaseClass implements OnInit { columnTitles = [ @@ -112,6 +120,25 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { ); } + /** Toggles a schedule from suspend to resume, or vice versa. */ + suspendSchedule(schedules: ScheduleSuspendResponse[]) { + this.scheduleService.suspendSchedule(schedules) + .subscribe( + (response) => { + if (response.schedules) { + let self = this; + response.schedules.forEach(function(schedule) { + const original = self.dataSource.data.filter(x => x.urlsafe_key === schedule.urlsafe_key); + if (original) { + original[0].suspended = schedule.suspend; + } + }) + } + }, + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) + ); + } + /** Hooks a page event and handles properly. */ onPageEvent(event: PageEvent) { this.pageSize = event.pageSize; diff --git a/gae/frontend/src/app/menu/schedule/schedule.service.ts b/gae/frontend/src/app/menu/schedule/schedule.service.ts index 86c831d..ae534dd 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.service.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.service.ts @@ -22,6 +22,7 @@ import { Observable } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ScheduleWrapper } from '../../model/schedule_wrapper'; import { ServiceBase } from '../../shared/servicebase'; +import { ScheduleSuspendResponse, ScheduleSuspendResponseWrapper } from '../../model/schedule'; @Injectable() @@ -41,4 +42,10 @@ export class ScheduleService extends ServiceBase { return this.httpClient.post(url, {size: size, offset: offset, filter: filterInfo, sort: sort, direction: direction}) .pipe(catchError(this.handleError)); } + + suspendSchedule(schedules: ScheduleSuspendResponse[]): Observable { + const url = this.url + 'suspend'; + return this.httpClient.post(url, {schedules: schedules}) + .pipe(catchError(this.handleError)); + } } diff --git a/gae/frontend/src/app/model/schedule.ts b/gae/frontend/src/app/model/schedule.ts index 756cb00..5046f2f 100644 --- a/gae/frontend/src/app/model/schedule.ts +++ b/gae/frontend/src/app/model/schedule.ts @@ -59,4 +59,16 @@ export class Schedule { image_package_repo_base: string = void 0; timestamp = void 0; owner: string[] = void 0; + + suspended: boolean = void 0; + urlsafe_key: string = void 0; +} + +export interface ScheduleSuspendResponseWrapper { + schedules: ScheduleSuspendResponse[]; +} + +export interface ScheduleSuspendResponse { + urlsafe_key: string; + suspend: boolean; } diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py index 0e429dd..d0dddd5 100644 --- a/gae/webapp/src/endpoint/endpoint_base.py +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -323,6 +323,8 @@ class EndpointBase(remote.Service): resource=entity, reference=message) for attr in assigned_attributes: entity_dict[attr] = getattr(entity, attr, None) + if hasattr(message, "urlsafe_key"): + entity_dict["urlsafe_key"] = entity.key.urlsafe() return_list.append(entity_dict) return return_list, more diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py index ce13a40..e353902 100644 --- a/gae/webapp/src/endpoint/schedule_info.py +++ b/gae/webapp/src/endpoint/schedule_info.py @@ -21,8 +21,11 @@ from google.appengine.ext import ndb from webapp.src import vtslab_status as Status from webapp.src.endpoint import endpoint_base from webapp.src.proto import model +from webapp.src.utils import email_util SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer(model.ScheduleInfoMessage) +SCHEDULE_SUSPEND_RESOURCE = endpoints.ResourceContainer( + model.ScheduleSuspendMessage) @endpoints.api(name="schedule", version="v1") @@ -134,6 +137,33 @@ class ScheduleInfoApi(endpoint_base.EndpointBase): return model.CountResponseMessage(count=count) + @endpoints.method( + SCHEDULE_SUSPEND_RESOURCE, + model.ScheduleSuspendMessage, + path="suspend", + http_method="POST", + name="suspend") + def suspend(self, request): + """Toggles a schedule from suspend to resume, or vice versa.""" + schedules_to_put = [] + schedules_to_return = [] + for schedule in request.schedules: + schedule_key = ndb.key.Key(urlsafe=schedule.urlsafe_key) + schedule_entity = schedule_key.get() + if schedule.suspend: # to suspend + schedule_entity.suspended = True + else: # to resume + schedule_entity.error_count = 0 + schedule_entity.suspended = False + schedules_to_put.append(schedule_entity) + schedules_to_return.append({"urlsafe_key": schedule.urlsafe_key, + "suspend": schedule_entity.suspended}) + # TODO(jongmok): Minimize a number of emails by merging schedules. + email_util.send_schedule_suspension_notification(schedule_entity) + + ndb.put_multi(schedules_to_put) + return model.ScheduleSuspendMessage(schedules=schedules_to_return) + @endpoints.api(name="green_schedule_info", version="v1") class GreenScheduleInfoApi(endpoint_base.EndpointBase): diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 52b7a8a..56b4566 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -114,7 +114,7 @@ class ScheduleControlInfoMessage(messages.Message): class ScheduleInfoMessage(messages.Message): """A message for representing an individual schedule entry.""" - # Next ID = 36 + # Next ID = 38 # schedule name for green build schedule, optional. name = messages.StringField(16) schedule_type = messages.StringField(19) @@ -162,6 +162,9 @@ class ScheduleInfoMessage(messages.Message): timestamp = message_types.DateTimeField(34) owner = messages.StringField(35, repeated=True) + suspended = messages.BooleanField(36) + urlsafe_key = messages.StringField(37) + class LabModel(ndb.Model): """A model for representing an individual lab entry.""" @@ -428,3 +431,14 @@ class CountRequestMessage(messages.Message): class CountResponseMessage(messages.Message): """A message of a count of entities to respond to /count endpoints.""" count = messages.IntegerField(1) + + +class ScheduleSuspendMessage(messages.Message): + """A response message to schedule endpoint API's /suspend method.""" + + class SingleScheduleSuspendMessage(messages.Message): + urlsafe_key = messages.StringField(1) + suspend = messages.BooleanField(2) + + schedules = messages.MessageField( + SingleScheduleSuspendMessage, 1, repeated=True) -- cgit v1.2.3 From 2c8485a1ed46095b42adecfa1473c370e37dcfe5 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Fri, 28 Sep 2018 06:20:08 +0900 Subject: Add a missing hostname property. This change includes ordering the list of devices by hostname, and a similar change is also applied to the job page to create an arrow in order to indicate users how the list of jobs is ordered. Test: go/vtslab-schedule-dev Bug: 116078450 Change-Id: If3f7f809d001f136a98979a63f57acdab00bc288 --- gae/frontend/src/app/app.module.ts | 3 +++ gae/frontend/src/app/menu/device/device.component.html | 7 +++---- gae/frontend/src/app/menu/device/device.component.ts | 8 +++++++- gae/frontend/src/app/menu/job/job.component.html | 2 +- gae/webapp/src/proto/model.py | 1 + 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index cacff6e..8eb7026 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -32,6 +32,7 @@ import { MatPaginatorModule } from '@angular/material/paginator'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSelectModule } from '@angular/material/select'; +import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -78,6 +79,7 @@ const appRoutes: Routes = [ MatProgressSpinnerModule, MatSnackBarModule, MatSelectModule, + MatSortModule, MatTableModule, MatTabsModule, ], @@ -92,6 +94,7 @@ const appRoutes: Routes = [ MatProgressSpinnerModule, MatSnackBarModule, MatSelectModule, + MatSortModule, MatTableModule, MatTabsModule, ] diff --git a/gae/frontend/src/app/menu/device/device.component.html b/gae/frontend/src/app/menu/device/device.component.html index 762e198..2ff28f4 100644 --- a/gae/frontend/src/app/menu/device/device.component.html +++ b/gae/frontend/src/app/menu/device/device.component.html @@ -16,7 +16,7 @@
-
+ No. @@ -25,7 +25,7 @@ - Host Name + Host Name {{device.hostname}} @@ -61,8 +61,7 @@ -
- + { this.loading = false; diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 62ce0f6..3c7220a 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -155,7 +155,7 @@ - + Timestamp {{getRelativeTime(job.timestamp)}} diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 52b7a8a..608c9b2 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -231,6 +231,7 @@ class DeviceInfoMessage(messages.Message): product = messages.StringField(2) status = messages.IntegerField(3) scheduling_status = messages.IntegerField(4) + hostname = messages.StringField(5) class HostInfoMessage(messages.Message): -- cgit v1.2.3 From b76820eae0c12db5ef5a3eeb828e98e2af6ddae7 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Wed, 19 Sep 2018 11:43:03 +0900 Subject: Display the latest build and schedule update time. Test: go/vtslab-schedule-dev Bug: 116284491 Change-Id: Ib1cf40aeb521a20db6be7736459c24d0329ab3d2 --- gae/frontend/src/app/app.module.ts | 3 ++ .../app/menu/dashboard/dashboard.component.html | 17 +++++++- .../app/menu/dashboard/dashboard.component.scss | 17 ++++++++ .../src/app/menu/dashboard/dashboard.component.ts | 51 ++++++++++++++++++++-- gae/frontend/src/app/menu/menu_base.ts | 2 +- gae/frontend/src/app/model/build.ts | 1 + gae/frontend/src/styles.scss | 4 ++ gae/webapp/src/proto/model.py | 1 + 8 files changed, 90 insertions(+), 6 deletions(-) diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index 8eb7026..c70fd52 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -23,6 +23,7 @@ import { RouterModule, Routes } from '@angular/router'; // Angular Material modules import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; import { MatChipsModule } from '@angular/material/chips'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -70,6 +71,7 @@ const appRoutes: Routes = [ @NgModule({ imports: [ MatButtonModule, + MatCardModule, MatChipsModule, MatExpansionModule, MatFormFieldModule, @@ -85,6 +87,7 @@ const appRoutes: Routes = [ ], exports: [ MatButtonModule, + MatCardModule, MatChipsModule, MatExpansionModule, MatFormFieldModule, diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.html b/gae/frontend/src/app/menu/dashboard/dashboard.component.html index 0ddc6ef..b91e80c 100644 --- a/gae/frontend/src/app/menu/dashboard/dashboard.component.html +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.html @@ -12,4 +12,19 @@ See the License for the specific language governing permissions and limitations under the License. --> -VTS Scheduler Dashboard +
+ + Build + Last updated: {{getRelativeTime(lastBuildUpdateTime)}} + + + + Schedule + Last updated: {{getRelativeTime(lastScheduleUpdateTime)}} + + +
diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.scss b/gae/frontend/src/app/menu/dashboard/dashboard.component.scss index e69de29..a17cb36 100644 --- a/gae/frontend/src/app/menu/dashboard/dashboard.component.scss +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.scss @@ -0,0 +1,17 @@ +.mat-card { + width: 50%; + + .mat-raised-button { + position: absolute; + top: 10px; + right: 10px; + min-width: 28px; + width: 28px; + height: 28px; + padding: 0; + .mat-icon { + width: 24px; + height: 24px; + } + } +} diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts index 57ea988..79d85a0 100644 --- a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts @@ -13,17 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatSnackBar } from '@angular/material'; +import { BuildService } from "../build/build.service"; +import { MenuBaseClass } from "../menu_base"; +import { ScheduleService } from "../schedule/schedule.service"; + /** Component that handles dashboard. */ @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', + providers: [ BuildService, ScheduleService ], styleUrls: ['./dashboard.component.scss'] }) -export class DashboardComponent { - constructor(public snackBar: MatSnackBar) { - this.snackBar.dismiss(); +export class DashboardComponent extends MenuBaseClass implements OnInit { + lastBuildUpdateTime: any = '---'; + lastScheduleUpdateTime: any = '---'; + + constructor(private buildService: BuildService, + private scheduleService: ScheduleService, + public snackBar: MatSnackBar) { + super(snackBar); + } + + ngOnInit(): void { + this.getLatestBuild(); + this.getLastestSchedule(); + } + + /** Fetches the most recently updated build and gets timestamp from it. */ + getLatestBuild() { + this.lastBuildUpdateTime = '---'; + this.buildService.getBuilds(1, 0, '', 'timestamp', 'desc') + .subscribe( + (response) => { + if (response.builds) { + this.lastBuildUpdateTime = response.builds[0].timestamp; + } + }, + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) + ); + } + + /** Fetches the most recently updated schedule and gets timestamp from it. */ + getLastestSchedule() { + this.lastScheduleUpdateTime = '---'; + this.scheduleService.getSchedules(1, 0, '', 'timestamp', 'desc') + .subscribe( + (response) => { + if (response.schedules) { + this.lastScheduleUpdateTime = response.schedules[0].timestamp; + } + }, + (error) => this.showSnackbar(`[${error.status}] ${error.name}`) + ); } } diff --git a/gae/frontend/src/app/menu/menu_base.ts b/gae/frontend/src/app/menu/menu_base.ts index 8d568b0..316923e 100644 --- a/gae/frontend/src/app/menu/menu_base.ts +++ b/gae/frontend/src/app/menu/menu_base.ts @@ -50,7 +50,7 @@ export abstract class MenuBaseClass { getRelativeTime(timeString) { return (moment.tz(timeString, 'YYYY-MM-DDThh:mm:ss', 'UTC').isValid() ? - moment.tz(timeString, 'YYYY-MM-DDThh:mm:ss', 'UTC').fromNow() : timeString); + moment.tz(timeString, 'YYYY-MM-DDThh:mm:ss', 'UTC').fromNow() : '---'); } /** Displays a snackbar notification. */ diff --git a/gae/frontend/src/app/model/build.ts b/gae/frontend/src/app/model/build.ts index 9e946d1..bf32a4a 100644 --- a/gae/frontend/src/app/model/build.ts +++ b/gae/frontend/src/app/model/build.ts @@ -21,4 +21,5 @@ export class Build { artifact_type: string = void 0; artifacts: string[] = void 0; signed: boolean = void 0; + timestamp: any = void 0; } diff --git a/gae/frontend/src/styles.scss b/gae/frontend/src/styles.scss index 0c95e64..7505757 100644 --- a/gae/frontend/src/styles.scss +++ b/gae/frontend/src/styles.scss @@ -22,6 +22,10 @@ body { } } +.mat-card { + margin: 20px; +} + .entity-table { margin: 10px 20px 20px 20px; diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index b29ad6b..d24db1e 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -42,6 +42,7 @@ class BuildInfoMessage(messages.Message): artifact_type = messages.StringField(5) artifacts = messages.StringField(6, repeated=True) signed = messages.BooleanField(7) + timestamp = message_types.DateTimeField(8) class ScheduleControlModel(ndb.Model): -- cgit v1.2.3 From be4f3513da99b641b86fe57a9c0171eb4fc525da Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 2 Oct 2018 10:33:18 +0900 Subject: Fix a job page crush issue. Test: go/vtslab-schedule-dev Bug: 116078450 --- gae/frontend/src/app/menu/job/job.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 3c7220a..5151783 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -155,8 +155,8 @@
- - Timestamp + + Timestamp {{getRelativeTime(job.timestamp)}} -- cgit v1.2.3 From 6cbe628b7c378aa6fd7b59dc06505adecb6b1af2 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Tue, 2 Oct 2018 08:54:47 +0900 Subject: Add a Show Details button to list all properties. Test: go/vtslab-schedule-dev/redirect/20181011t154848-dot-vtslab-schedule-dev Bug: 117531288 Change-Id: If2ae84baf01d64d83e5918e5bd0dfdff1ddc31c8 --- gae/frontend/src/app/app.component.html | 17 +++++++++- gae/frontend/src/app/app.component.scss | 28 ++++++++++++++++ gae/frontend/src/app/app.component.ts | 37 ++++++++++++++++++++++ gae/frontend/src/app/app.module.ts | 6 ++++ gae/frontend/src/app/appservice.ts | 37 ++++++++++++++++++++++ gae/frontend/src/app/menu/build/build.component.ts | 6 ++-- .../src/app/menu/dashboard/dashboard.component.ts | 6 ++-- .../src/app/menu/device/device.component.ts | 6 ++-- gae/frontend/src/app/menu/job/job.component.html | 3 ++ gae/frontend/src/app/menu/job/job.component.scss | 17 ++++++++++ gae/frontend/src/app/menu/job/job.component.ts | 6 ++-- gae/frontend/src/app/menu/lab/lab.component.ts | 6 ++-- gae/frontend/src/app/menu/menu_base.ts | 10 +++++- .../src/app/menu/schedule/schedule.component.html | 3 ++ .../src/app/menu/schedule/schedule.component.scss | 4 --- .../src/app/menu/schedule/schedule.component.ts | 6 ++-- gae/frontend/src/styles.scss | 6 +++- 17 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 gae/frontend/src/app/appservice.ts diff --git a/gae/frontend/src/app/app.component.html b/gae/frontend/src/app/app.component.html index a4df15a..8f21391 100644 --- a/gae/frontend/src/app/app.component.html +++ b/gae/frontend/src/app/app.component.html @@ -17,4 +17,19 @@
- + + + + + + + + +

{{property.name}}

+

{{each}}

+
+
+
+
diff --git a/gae/frontend/src/app/app.component.scss b/gae/frontend/src/app/app.component.scss index e69de29..d818d0e 100644 --- a/gae/frontend/src/app/app.component.scss +++ b/gae/frontend/src/app/app.component.scss @@ -0,0 +1,28 @@ +mat-sidenav { + width: 400px; + padding: 30px 10px; +} + +#property-name { + color: rgba(0, 0, 0, 0.66); + font-size: 12px; + margin-bottom: 2px; +} + +#property-value { + font-size: 12px; +} + +.mat-button { + position: absolute; + top: 10px; + right: 10px; + min-width: 28px; + width: 28px; + height: 28px; + padding: 0; + .mat-icon { + width: 24px; + height: 24px; + } +} diff --git a/gae/frontend/src/app/app.component.ts b/gae/frontend/src/app/app.component.ts index 9e83762..68b7e6d 100644 --- a/gae/frontend/src/app/app.component.ts +++ b/gae/frontend/src/app/app.component.ts @@ -16,10 +16,47 @@ import { Component } from '@angular/core'; +import { AppService } from "./appservice"; + + @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { + _sideNavOpened = false; + get sideNavOpened(): boolean { + return this._sideNavOpened; + } + set sideNavOpened(value: boolean) { + this._sideNavOpened = value; + if (!value) { + this.selectedEntity = this.selectedEntity.slice(); + } + } + selectedEntity: {name: string; value: any[]}[] = []; + + constructor(private appService: AppService) { + appService.closeSideNavEmitter.subscribe(() => {this.sideNavOpened = false}); + appService.showDetailsEmitter.subscribe( + (entity) => { + if (entity) { + let self = this; + Object.keys(entity).forEach(function(value){ + if (value !== 'urlsafe_key') { + self.selectedEntity.push({ + name: value, + value: (entity[value] instanceof Array) ? entity[value] : [entity[value]] + }); + } + }); + } + this.sideNavOpened = !this.sideNavOpened; + }, + (error) => { + console.log(error); + } + ) + } } diff --git a/gae/frontend/src/app/app.module.ts b/gae/frontend/src/app/app.module.ts index c70fd52..ec940db 100644 --- a/gae/frontend/src/app/app.module.ts +++ b/gae/frontend/src/app/app.module.ts @@ -29,10 +29,12 @@ import { MatExpansionModule } from '@angular/material/expansion'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material'; import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -77,10 +79,12 @@ const appRoutes: Routes = [ MatFormFieldModule, MatIconModule, MatInputModule, + MatListModule, MatPaginatorModule, MatProgressSpinnerModule, MatSnackBarModule, MatSelectModule, + MatSidenavModule, MatSortModule, MatTableModule, MatTabsModule, @@ -93,10 +97,12 @@ const appRoutes: Routes = [ MatFormFieldModule, MatIconModule, MatInputModule, + MatListModule, MatPaginatorModule, MatProgressSpinnerModule, MatSnackBarModule, MatSelectModule, + MatSidenavModule, MatSortModule, MatTableModule, MatTabsModule, diff --git a/gae/frontend/src/app/appservice.ts b/gae/frontend/src/app/appservice.ts new file mode 100644 index 0000000..6b303f0 --- /dev/null +++ b/gae/frontend/src/app/appservice.ts @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {EventEmitter, Injectable, Output} from '@angular/core'; + + +@Injectable({ + providedIn: 'root', +}) +export class AppService { + @Output() closeSideNavEmitter = new EventEmitter(); + @Output() showDetailsEmitter = new EventEmitter(); + constructor() { + } + + /** Emits an EventEmitter to display entity in the side nav window. */ + showDetails(entity) { + this.showDetailsEmitter.emit(entity); + } + + /** Emits an EventEmitter to close the side nav window. */ + closeSideNav() { + this.closeSideNavEmitter.emit(); + } +} diff --git a/gae/frontend/src/app/menu/build/build.component.ts b/gae/frontend/src/app/menu/build/build.component.ts index e149f87..e4c7325 100644 --- a/gae/frontend/src/app/menu/build/build.component.ts +++ b/gae/frontend/src/app/menu/build/build.component.ts @@ -16,6 +16,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; +import { AppService } from '../../appservice'; import { Build } from '../../model/build'; import { BuildService } from './build.service'; import { FilterComponent } from '../../shared/filter/filter.component'; @@ -46,8 +47,9 @@ export class BuildComponent extends MenuBaseClass implements OnInit { @ViewChild(FilterComponent) filterComponent: FilterComponent; constructor(private buildService: BuildService, - public snackBar: MatSnackBar) { - super(snackBar); + appService: AppService, + snackBar: MatSnackBar) { + super(appService, snackBar); } ngOnInit(): void { diff --git a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts index 79d85a0..0157ea8 100644 --- a/gae/frontend/src/app/menu/dashboard/dashboard.component.ts +++ b/gae/frontend/src/app/menu/dashboard/dashboard.component.ts @@ -16,6 +16,7 @@ import { Component, OnInit } from '@angular/core'; import { MatSnackBar } from '@angular/material'; +import { AppService } from '../../appservice'; import { BuildService } from "../build/build.service"; import { MenuBaseClass } from "../menu_base"; import { ScheduleService } from "../schedule/schedule.service"; @@ -33,8 +34,9 @@ export class DashboardComponent extends MenuBaseClass implements OnInit { constructor(private buildService: BuildService, private scheduleService: ScheduleService, - public snackBar: MatSnackBar) { - super(snackBar); + appService: AppService, + snackBar: MatSnackBar) { + super(appService, snackBar); } ngOnInit(): void { diff --git a/gae/frontend/src/app/menu/device/device.component.ts b/gae/frontend/src/app/menu/device/device.component.ts index 31407d5..2fc9a72 100644 --- a/gae/frontend/src/app/menu/device/device.component.ts +++ b/gae/frontend/src/app/menu/device/device.component.ts @@ -16,6 +16,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; +import { AppService } from '../../appservice'; import { Device } from '../../model/device'; import { DeviceService } from './device.service'; import { DeviceStatus, SchedulingStatus } from '../../shared/vtslab_status'; @@ -53,8 +54,9 @@ export class DeviceComponent extends MenuBaseClass implements OnInit { @ViewChild(FilterComponent) filterComponent: FilterComponent; constructor(private deviceService: DeviceService, - public snackBar: MatSnackBar) { - super(snackBar); + appService: AppService, + snackBar: MatSnackBar) { + super(appService, snackBar); } ngOnInit(): void { diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index 5151783..634402d 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -182,6 +182,9 @@
+
diff --git a/gae/frontend/src/app/menu/job/job.component.scss b/gae/frontend/src/app/menu/job/job.component.scss index 165de43..c8aee00 100644 --- a/gae/frontend/src/app/menu/job/job.component.scss +++ b/gae/frontend/src/app/menu/job/job.component.scss @@ -5,3 +5,20 @@ .mat-cell { padding: 0 10px 0 10px; } + +.element-row { + position: relative; + overflow: hidden; +} + +.element-row:not(.expanded) { + cursor: pointer; +} + +.element-row:not(.expanded):hover { + background: #f5f5f5; +} + +.element-row.expanded { + border-bottom-color: transparent; +} diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index 383ae19..4375581 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -17,6 +17,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { animate, state, style, transition, trigger } from '@angular/animations'; +import { AppService } from '../../appservice'; import { FilterComponent } from '../../shared/filter/filter.component'; import { FilterCondition } from '../../model/filter_condition'; import { FilterItem } from '../../model/filter_item'; @@ -84,8 +85,9 @@ export class JobComponent extends MenuBaseClass implements OnInit { sortDirection = ''; constructor(private jobService: JobService, - public snackBar: MatSnackBar) { - super(snackBar); + appService: AppService, + snackBar: MatSnackBar) { + super(appService, snackBar); } ngOnInit(): void { diff --git a/gae/frontend/src/app/menu/lab/lab.component.ts b/gae/frontend/src/app/menu/lab/lab.component.ts index 83fe3a1..bb7543b 100644 --- a/gae/frontend/src/app/menu/lab/lab.component.ts +++ b/gae/frontend/src/app/menu/lab/lab.component.ts @@ -16,6 +16,7 @@ import { Component, OnInit } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; +import { AppService } from '../../appservice'; import { Host } from '../../model/host'; import { Lab } from '../../model/lab'; import { LabService } from './lab.service'; @@ -48,8 +49,9 @@ export class LabComponent extends MenuBaseClass implements OnInit { labPageIndex = 0; constructor(private labService: LabService, - public snackBar: MatSnackBar) { - super(snackBar); + appService: AppService, + snackBar: MatSnackBar) { + super(appService, snackBar); } labDataSource = new MatTableDataSource(); diff --git a/gae/frontend/src/app/menu/menu_base.ts b/gae/frontend/src/app/menu/menu_base.ts index 316923e..9282fe5 100644 --- a/gae/frontend/src/app/menu/menu_base.ts +++ b/gae/frontend/src/app/menu/menu_base.ts @@ -17,6 +17,7 @@ /** This class defines and/or implements the common properties and methods * used among menus. */ +import { AppService } from '../appservice'; import { MatSnackBar } from '@angular/material'; import moment from 'moment-timezone'; @@ -29,7 +30,9 @@ export abstract class MenuBaseClass { pageSize = 100; pageIndex = 0; - protected constructor(public snackBar: MatSnackBar) { + protected constructor(private appService: AppService, + public snackBar: MatSnackBar) { + this.appService.closeSideNav(); this.snackBar.dismiss(); } @@ -58,4 +61,9 @@ export abstract class MenuBaseClass { this.loading = false; this.snackBar.open(message, 'DISMISS', {duration}); } + + /** Displays a side nav window and lists all properties of selected entity. */ + onShowDetailsClicked(entity) { + this.appService.showDetails(entity); + } } diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html index c567639..e260325 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.html +++ b/gae/frontend/src/app/menu/schedule/schedule.component.html @@ -102,6 +102,9 @@ +
diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.scss b/gae/frontend/src/app/menu/schedule/schedule.component.scss index a1e3e98..c8aee00 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.scss +++ b/gae/frontend/src/app/menu/schedule/schedule.component.scss @@ -22,7 +22,3 @@ .element-row.expanded { border-bottom-color: transparent; } - -.div-expandable { - padding: 10px 20px 30px 20px; -} diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts index 57ebd7b..6c72343 100644 --- a/gae/frontend/src/app/menu/schedule/schedule.component.ts +++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts @@ -17,6 +17,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { animate, state, style, transition, trigger } from "@angular/animations"; +import { AppService } from '../../appservice'; import { FilterComponent } from '../../shared/filter/filter.component'; import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; @@ -59,8 +60,9 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit { @ViewChild(FilterComponent) filterComponent: FilterComponent; constructor(private scheduleService: ScheduleService, - public snackBar: MatSnackBar) { - super(snackBar); + appService: AppService, + snackBar: MatSnackBar) { + super(appService, snackBar); } ngOnInit(): void { diff --git a/gae/frontend/src/styles.scss b/gae/frontend/src/styles.scss index 7505757..574f294 100644 --- a/gae/frontend/src/styles.scss +++ b/gae/frontend/src/styles.scss @@ -46,5 +46,9 @@ body { } .div-expandable { - padding: 10px; + padding: 10px 20px 30px 20px; + + button { + margin-right: 20px; + } } -- cgit v1.2.3 From adcd63b696217b05516f22c9def512edc86f45c8 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Fri, 12 Oct 2018 10:53:16 +0900 Subject: Add a missing device equipment on device page. Test: go/vtslab-schedule-test/redirect/20181012t104842-dot-vtslab-schedule-test Bug: 117585453 --- gae/webapp/src/proto/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index d24db1e..375dcc4 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -236,6 +236,7 @@ class DeviceInfoMessage(messages.Message): status = messages.IntegerField(3) scheduling_status = messages.IntegerField(4) hostname = messages.StringField(5) + device_equipment = messages.StringField(6, repeated=True) class HostInfoMessage(messages.Message): -- cgit v1.2.3 From b39e49f88019d7fbb140f7a59f8c64a8a9c9bb46 Mon Sep 17 00:00:00 2001 From: Jongmok Hong Date: Mon, 15 Oct 2018 15:56:48 +0900 Subject: Remove old frontend pages. Test: ./script/deploy-webapp.sh local Bug: 74575555 --- gae/.gitignore | 1 + gae/webapp/src/dashboard/__init__.py | 0 gae/webapp/src/dashboard/build_list.py | 142 - gae/webapp/src/dashboard/device_list.py | 61 - gae/webapp/src/dashboard/device_stats.py | 107 - gae/webapp/src/dashboard/job_list.py | 268 - gae/webapp/src/dashboard/schedule_list.py | 97 - .../static/bootstrap/css/bootstrap-responsive.css | 1109 ---- .../bootstrap/css/bootstrap-responsive.min.css | 9 - gae/webapp/static/bootstrap/css/bootstrap.css | 6158 -------------------- gae/webapp/static/bootstrap/css/bootstrap.min.css | 9 - .../bootstrap/img/glyphicons-halflings-white.png | Bin 8777 -> 0 bytes .../static/bootstrap/img/glyphicons-halflings.png | Bin 12799 -> 0 bytes gae/webapp/static/bootstrap/js/bootstrap.js | 2276 -------- gae/webapp/static/bootstrap/js/bootstrap.min.js | 6 - gae/webapp/static/build.html | 182 - gae/webapp/static/create_job_template.html | 196 - gae/webapp/static/device.html | 176 - gae/webapp/static/index.html | 63 - gae/webapp/static/job.html | 272 - gae/webapp/static/schedule.html | 128 - 21 files changed, 1 insertion(+), 11259 deletions(-) create mode 100644 gae/.gitignore delete mode 100644 gae/webapp/src/dashboard/__init__.py delete mode 100644 gae/webapp/src/dashboard/build_list.py delete mode 100644 gae/webapp/src/dashboard/device_list.py delete mode 100644 gae/webapp/src/dashboard/device_stats.py delete mode 100644 gae/webapp/src/dashboard/job_list.py delete mode 100644 gae/webapp/src/dashboard/schedule_list.py delete mode 100644 gae/webapp/static/bootstrap/css/bootstrap-responsive.css delete mode 100644 gae/webapp/static/bootstrap/css/bootstrap-responsive.min.css delete mode 100644 gae/webapp/static/bootstrap/css/bootstrap.css delete mode 100644 gae/webapp/static/bootstrap/css/bootstrap.min.css delete mode 100644 gae/webapp/static/bootstrap/img/glyphicons-halflings-white.png delete mode 100644 gae/webapp/static/bootstrap/img/glyphicons-halflings.png delete mode 100644 gae/webapp/static/bootstrap/js/bootstrap.js delete mode 100644 gae/webapp/static/bootstrap/js/bootstrap.min.js delete mode 100644 gae/webapp/static/build.html delete mode 100644 gae/webapp/static/create_job_template.html delete mode 100644 gae/webapp/static/device.html delete mode 100644 gae/webapp/static/index.html delete mode 100644 gae/webapp/static/job.html delete mode 100644 gae/webapp/static/schedule.html diff --git a/gae/.gitignore b/gae/.gitignore new file mode 100644 index 0000000..9d1699c --- /dev/null +++ b/gae/.gitignore @@ -0,0 +1 @@ +webapp/static/** diff --git a/gae/webapp/src/dashboard/__init__.py b/gae/webapp/src/dashboard/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gae/webapp/src/dashboard/build_list.py b/gae/webapp/src/dashboard/build_list.py deleted file mode 100644 index f0475a8..0000000 --- a/gae/webapp/src/dashboard/build_list.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2017 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import datetime -import re - -from webapp.src.handlers import base -from webapp.src.proto import model - - -def ReadBuildInfo(target_branch=""): - """Reads build information. - - Args: - target_branch: string, to select a specific branch. - - Returns: - a dict containing test build information, - a dict containing device build information, - a dict containing gsi build information. - """ - build_query = model.BuildModel.query( - model.BuildModel.timestamp > - datetime.datetime.now() - datetime.timedelta(days=2)) - builds = build_query.fetch() - - test_builds = {} - device_builds = {} - gsi_builds = {} - - gcs_pattern = "^gs://.*" - q_pattern = "(git_)?(aosp-)?q.*" - p_pattern = "(git_)?(aosp-)?p.*" - o_mr1_pattern = "(git_)?(aosp-)?o[^-]*-m.*" - o_pattern = "(git_)?(aosp-)?o.*" - - if builds: - for build in builds: - if re.match(gcs_pattern, build.manifest_branch): - m_branch = "GCS" - elif re.match(q_pattern, build.manifest_branch): - m_branch = "Q" - elif re.match(p_pattern, build.manifest_branch): - m_branch = "P" - elif re.match(o_mr1_pattern, build.manifest_branch): - m_branch = "O-MR1" - elif re.match(o_pattern, build.manifest_branch): - m_branch = "O" - else: - m_branch = "Unknown" - - if target_branch and target_branch != m_branch: - continue - - if build.manifest_branch.startswith("git_"): - build.manifest_branch = build.manifest_branch.replace("git_", "") - - if build.artifact_type == "test": - if m_branch in test_builds: - test_builds[m_branch].append(build) - else: - test_builds[m_branch] = [build] - elif build.artifact_type == "device": - if m_branch in device_builds: - device_builds[m_branch].append(build) - else: - device_builds[m_branch] = [build] - elif build.artifact_type == "gsi": - if m_branch in gsi_builds: - gsi_builds[m_branch].append(build) - else: - gsi_builds[m_branch] = [build] - else: - print("unknown artifact_type %s" % build.artifact_type) - - if test_builds: - for m_branch in test_builds: - test_builds[m_branch] = sorted( - test_builds[m_branch], key=lambda x: x.build_id, reverse=True) - if device_builds: - for m_branch in device_builds: - device_builds[m_branch] = sorted( - device_builds[m_branch], key=lambda x: x.build_id, reverse=True) - if gsi_builds: - for m_branch in gsi_builds: - gsi_builds[m_branch] = sorted( - gsi_builds[m_branch], key=lambda x: x.build_id, reverse=True) - return test_builds, device_builds, gsi_builds - - -class BuildPage(base.BaseHandler): - """Main class for /build web page.""" - - def get(self): - """Generates an HTML page based on the build info kept in DB.""" - self.template = "build.html" - - target_branch = self.request.get("branch", default_value="") - - test_builds, device_builds, gsi_builds = ReadBuildInfo(target_branch) - - manifest_branch_keys = list(set().union( - test_builds.keys(), device_builds.keys(), - gsi_builds.keys())) - all_builds = {} - for manifest_branch_key in manifest_branch_keys: - all_builds[manifest_branch_key] = {} - if manifest_branch_key in test_builds: - all_builds[manifest_branch_key]["test"] = test_builds[ - manifest_branch_key] - else: - all_builds[manifest_branch_key]["test"] = [] - if manifest_branch_key in device_builds: - all_builds[manifest_branch_key]["device"] = device_builds[ - manifest_branch_key] - else: - all_builds[manifest_branch_key]["device"] = [] - if manifest_branch_key in gsi_builds: - all_builds[manifest_branch_key]["gsi"] = gsi_builds[ - manifest_branch_key] - else: - all_builds[manifest_branch_key]["gsi"] = [] - - template_values = { - "all_builds": all_builds - } - - self.render(template_values) diff --git a/gae/webapp/src/dashboard/device_list.py b/gae/webapp/src/dashboard/device_list.py deleted file mode 100644 index 371f858..0000000 --- a/gae/webapp/src/dashboard/device_list.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2017 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from webapp.src.handlers import base -from webapp.src.proto import model -from webapp.src import vtslab_status -from webapp.src.dashboard import device_stats - - -class DevicePage(base.BaseHandler): - """Main class for /device web page.""" - - def get(self): - """Generates an HTML page based on the device info kept in DB.""" - self.template = "device.html" - - device_query = model.DeviceModel.query() - devices = device_query.fetch() - - lab_query = model.LabModel.query() - labs = lab_query.fetch() - - stats = device_stats.DeviceStats() - if devices: - devices = sorted( - devices, key=lambda x: (x.hostname, x.product, x.status), - reverse=False) - for device in devices: - device_product_lowcase = device.product.lower() - if device.scheduling_status == vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["free"]: - if (device.status == vtslab_status.DEVICE_STATUS_DICT["error"] - or device.status == vtslab_status.DEVICE_STATUS_DICT["no-response"]): - stats.add_error(device_product_lowcase) - else: - # it shouldn't be in use state. - stats.add_idle(device_product_lowcase) - else: - # includes both use and reserved - stats.add_active(device_product_lowcase) - - template_values = { - "devices": devices, - "labs": labs, - "stats": stats - } - - self.render(template_values) diff --git a/gae/webapp/src/dashboard/device_stats.py b/gae/webapp/src/dashboard/device_stats.py deleted file mode 100644 index 9fbf64f..0000000 --- a/gae/webapp/src/dashboard/device_stats.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2018 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - - -class DeviceStats(object): - """Device stats class. - - Attributes: - _total: int, total device count. - _active: int, device count in active state. - _error: int, device count in error state. - _idle: int, device count in ready state. - """ - - def __init__(self, parent=True): - self._total = 0 - self._active = 0 - self._error = 0 - self._idle = 0 - if parent == True: - self._product_stat = {} - - @property - def total(self): - return self._total - - @total.setter - def total(self, total): - self._total = total - - @property - def active(self): - return self._active - - @active.setter - def active(self, active): - self._active = active - - @property - def error(self): - return self._error - - @error.setter - def error(self, error): - self._error = error - - @property - def idle(self): - return self._idle - - @idle.setter - def idle(self, idle): - self._idle = idle - - @property - def utilization(self): - return self._active * 100 / self._total - - @property - def error_ratio(self): - return self._error * 100 / self._total - - @property - def product_stat(self): - return self._product_stat - - def __getitem__(self, product): - return self._product_stat[product] - - def _add_total(self, product=""): - self._total += 1 - if product: - if product not in self._product_stat: - self._product_stat[product] = DeviceStats(False) - self._product_stat[product].total += 1 - - def add_active(self, product=""): - self._add_total(product) - self._active += 1 - if product: - self._product_stat[product].active += 1 - - def add_error(self, product=""): - self._add_total(product) - self._error += 1 - if product: - self._product_stat[product].error += 1 - - def add_idle(self, product=""): - self._add_total(product) - self._idle += 1 - if product: - self._product_stat[product].idle += 1 \ No newline at end of file diff --git a/gae/webapp/src/dashboard/job_list.py b/gae/webapp/src/dashboard/job_list.py deleted file mode 100644 index 6408367..0000000 --- a/gae/webapp/src/dashboard/job_list.py +++ /dev/null @@ -1,268 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2017 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import datetime - -from webapp.src import vtslab_status -from webapp.src.handlers import base -from webapp.src.scheduler import schedule_worker -from webapp.src.proto import model - -from google.appengine.ext import ndb - - -def test_type_text(test_type, join_str=", "): - """Generates text to represent in HTML with given test type. - - Args: - test_type: an integer, test type value. - join_str: a string, join separator. - - Returns: - A string of test type. - """ - text_list = [] - - if not test_type: - return "Unknown" - - if (test_type & 3) == ( - vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_UNKNOWN]): - return "Unknown" - - if test_type & vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_TOT]: - text_list.append("ToT") - if test_type & vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_OTA]: - text_list.append("OTA") - if test_type & vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_SIGNED]: - text_list.append("Signed") - - return join_str.join(text_list) - - -class JobStats(object): - """Job stats class. - - Attributes: - boot_error: int, the number of boot-up error jobs. - created: int, the number of created jobs. - completed: int, the number of completed jobs. - expired: int, the number of expired jobs. - infra_error: int, the number of infra error jobs. - running: int, the number of running jobs. - ready: int, the number of ready jobs. - unknown: int, the number of unknown jobs. - """ - - boot_error = 0 - created = 0 - completed = 0 - expired = 0 - infra_error = 0 - running = 0 - ready = 0 - unknown = 0 - - -class JobBase(base.BaseHandler): - """Base class for job pages.""" - - def _UpdateStats(self, stats, job): - """Updates the stats using the state info of a given job. - - Args: - stats: JobStats, the stats class to update. - job: JobModel, the job to check. - """ - stats.created += 1 - if job.status == vtslab_status.JOB_STATUS_DICT["complete"]: - stats.completed += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["leased"]: - stats.running += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["ready"]: - stats.ready += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["infra-err"]: - stats.infra_error += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["bootup-err"]: - stats.boot_error += 1 - elif job.status == vtslab_status.JOB_STATUS_DICT["expired"]: - stats.expired += 1 - else: - stats.unknown += 1 - - -class JobPage(JobBase): - """Main class for /job web page.""" - - def get(self): - """Generates an HTML page based on the job queue info kept in DB.""" - self.template = "job.html" - - job_query = model.JobModel.query() - jobs = job_query.fetch() - - now = datetime.datetime.now() - stats_all = JobStats() - stats_24hrs = JobStats() - stats_72hrs = JobStats() - if jobs: - for job in jobs: - self._UpdateStats(stats_all, job) - if now - job.timestamp <= datetime.timedelta(hours=24): - self._UpdateStats(stats_24hrs, job) - if now - job.timestamp <= datetime.timedelta(hours=72): - self._UpdateStats(stats_72hrs, job) - - template_values = { - "jobs": sorted(jobs, key=lambda x: x.timestamp, - reverse=True), - "stats_all": stats_all, - "stats_24hrs": stats_24hrs, - "stats_72hrs": stats_72hrs, - "test_type_text": test_type_text - } - - self.render(template_values) - - -class CreateJobTemplatePage(base.BaseHandler): - """Main class for /create_job_template web page.""" - - def get(self): - """Generates an HTML page to get custom job info.""" - self.template = "create_job_template.html" - template_values = {} - self.render(template_values) - - -class CreateJobPage(JobBase): - """Main class for /create_job web page.""" - - def get(self): - """Generates an HTML page that stores the provided custom job info to DB.""" - self.template = "job.html" - - serials = self.request.get("serial", default_value="").split(",") - - # TODO: check serial >= shards and select only required ones - device_query = model.DeviceModel.query(model.DeviceModel.serial.IN( - serials)) - devices = device_query.fetch() - error_devices = [] - for device in devices: - if device.scheduling_status in [ - vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["reserved"], - vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["use"] - ]: - error_devices.append(device.serial) - - if error_devices: - message = "Can't create a job because at some devices " \ - "are not available (%s)." % error_devices - else: - devices_to_put = [] - for device in devices: - device.scheduling_status = ( - vtslab_status.DEVICE_SCHEDULING_STATUS_DICT["reserved"]) - devices_to_put.append(device) - if devices_to_put: - ndb.put_multi(devices_to_put) - - message = "A new job is created!" - - new_job = model.JobModel() - new_job.hostname = self.request.get("hostname", default_value="") - new_job.priority = str(vtslab_status.GetPriorityValue( - self.request.get("priority", default_value="high"))) - new_job.test_name = self.request.get("test_name", default_value="") - new_job.device = self.request.get("device", default_value="") - new_job.period = int(self.request.get("period", - default_value="high")) - new_job.serial.extend(serials) - - new_job.manifest_branch = self.request.get("manifest_branch", - default_value="") - new_job.build_target = self.request.get("build_target", - default_value="") - - new_job.shards = int(self.request.get( - "shards", default_value=str(len(new_job.serial)))) - new_job.param = self.request.get("param", - default_value=[]).split(",") - - new_job.gsi_branch = self.request.get("gsi_branch", - default_value="") - new_job.gsi_build_target = self.request.get("gsi_build_target", - default_value="") - new_job.gsi_build_id = self.request.get("gsi_build_id", - default_value="latest") - new_job.gsi_pab_account_id = self.request.get("gsi_pab_account_id", - default_value="") - - new_job.test_branch = self.request.get("test_branch", - default_value="") - new_job.test_build_target = self.request.get("test_build_target", - default_value="") - new_job.test_build_id = self.request.get("test_build_id", - default_value="latest") - new_job.test_pab_account_id = self.request.get( - "test_pab_account_id", default_value="") - - new_job.build_id = self.request.get("build_id", - default_value="latest") - new_job.pab_account_id = self.request.get("pab_account_id", - default_value="") - new_job.status = vtslab_status.JOB_STATUS_DICT["ready"] - new_job.timestamp = datetime.datetime.now() - - test_type = schedule_worker.GetTestVersionType( - new_job.manifest_branch, new_job.gsi_branch) - if new_job.require_signed_device_build: - test_type |= vtslab_status.TEST_TYPE_DICT[ - vtslab_status.TEST_TYPE_SIGNED] - test_type |= ( - vtslab_status.TEST_TYPE_DICT[vtslab_status.TEST_TYPE_MANUAL]) - new_job.test_type = test_type - - new_job.put() - - job_query = model.JobModel.query() - jobs = job_query.fetch() - - now = datetime.datetime.now() - stats_all = JobStats() - stats_24hrs = JobStats() - stats_72hrs = JobStats() - if jobs: - for job in jobs: - self._UpdateStats(stats_all, job) - if now - job.timestamp <= datetime.timedelta(hours=24): - self._UpdateStats(stats_24hrs, job) - if now - job.timestamp <= datetime.timedelta(hours=72): - self._UpdateStats(stats_72hrs, job) - - template_values = { - "message": message, - "jobs": sorted(jobs, key=lambda x: x.timestamp, - reverse=True), - "stats_all": stats_all, - "stats_24hrs": stats_24hrs, - "stats_72hrs": stats_72hrs, - "test_type_text": test_type_text - } - - self.render(template_values) diff --git a/gae/webapp/src/dashboard/schedule_list.py b/gae/webapp/src/dashboard/schedule_list.py deleted file mode 100644 index f7266f9..0000000 --- a/gae/webapp/src/dashboard/schedule_list.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2017 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from google.appengine.api import taskqueue -from google.appengine.ext import ndb - -from webapp.src.handlers import base -from webapp.src.proto import model -from webapp.src.utils import email_util - - -class SchedulePage(base.BaseHandler): - """Main class for /schedule web page.""" - - def get(self): - """Generates an HTML page based on the task schedules kept in DB.""" - self.template = "schedule.html" - - resume_key = self.request.get("resume") - if resume_key: - schedule_key = ndb.key.Key(urlsafe=resume_key) - schedule = schedule_key.get() - schedule.error_count = 0 - schedule.suspended = False - schedule.put() - email_util.send_schedule_suspension_notification(schedule) - - suspend_key = self.request.get("suspend") - if suspend_key: - schedule_key = ndb.key.Key(urlsafe=suspend_key) - schedule = schedule_key.get() - schedule.suspended = True - schedule.put() - email_util.send_schedule_suspension_notification(schedule) - - create_job_key = self.request.get("create_job") - if create_job_key: - taskqueue.add( - url="/worker/schedule_handler", - target="worker", - queue_name="queue-schedule", - transactional=False, - params={ - "schedule_key": create_job_key - }) - - toggle = self.request.get("schedule_enable_status_toggle", default_value="0") - - schedule_control = model.ScheduleControlModel.query() - schedule_control_dataset = schedule_control.fetch() - enabled = True - if schedule_control_dataset: - for schedule_control_data_tuple in schedule_control_dataset: - if (not schedule_control_data_tuple.schedule_name or - schedule_control_data_tuple.schedule_name == "global"): - enabled = schedule_control_data_tuple.enabled - if toggle == "1": - enabled = not enabled - schedule_control_data_tuple.enabled = enabled - schedule_control_data_tuple.put() - toggle = "0" - break - - if toggle == "1": - schedule_control_data_tuple = model.ScheduleControlModel() - enabled = not enabled - schedule_control_data_tuple.enabled = enabled - schedule_control_data_tuple.put() - - schedule_query = model.ScheduleModel.query() - schedules = schedule_query.fetch() - - if schedules: - schedules = sorted( - schedules, key=lambda x: (x.manifest_branch, x.build_target), - reverse=False) - - template_values = { - "schedules": schedules, - "enabled": enabled - } - - self.render(template_values) diff --git a/gae/webapp/static/bootstrap/css/bootstrap-responsive.css b/gae/webapp/static/bootstrap/css/bootstrap-responsive.css deleted file mode 100644 index fcd72f7..0000000 --- a/gae/webapp/static/bootstrap/css/bootstrap-responsive.css +++ /dev/null @@ -1,1109 +0,0 @@ -/*! - * Bootstrap Responsive v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ - -.clearfix { - *zoom: 1; -} - -.clearfix:before, -.clearfix:after { - display: table; - line-height: 0; - content: ""; -} - -.clearfix:after { - clear: both; -} - -.hide-text { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -.input-block-level { - display: block; - width: 100%; - min-height: 30px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -@-ms-viewport { - width: device-width; -} - -.hidden { - display: none; - visibility: hidden; -} - -.visible-phone { - display: none !important; -} - -.visible-tablet { - display: none !important; -} - -.hidden-desktop { - display: none !important; -} - -.visible-desktop { - display: inherit !important; -} - -@media (min-width: 768px) and (max-width: 979px) { - .hidden-desktop { - display: inherit !important; - } - .visible-desktop { - display: none !important ; - } - .visible-tablet { - display: inherit !important; - } - .hidden-tablet { - display: none !important; - } -} - -@media (max-width: 767px) { - .hidden-desktop { - display: inherit !important; - } - .visible-desktop { - display: none !important; - } - .visible-phone { - display: inherit !important; - } - .hidden-phone { - display: none !important; - } -} - -.visible-print { - display: none !important; -} - -@media print { - .visible-print { - display: inherit !important; - } - .hidden-print { - display: none !important; - } -} - -@media (min-width: 1200px) { - .row { - margin-left: -30px; - *zoom: 1; - } - .row:before, - .row:after { - display: table; - line-height: 0; - content: ""; - } - .row:after { - clear: both; - } - [class*="span"] { - float: left; - min-height: 1px; - margin-left: 30px; - } - .container, - .navbar-static-top .container, - .navbar-fixed-top .container, - .navbar-fixed-bottom .container { - width: 1170px; - } - .span12 { - width: 1170px; - } - .span11 { - width: 1070px; - } - .span10 { - width: 970px; - } - .span9 { - width: 870px; - } - .span8 { - width: 770px; - } - .span7 { - width: 670px; - } - .span6 { - width: 570px; - } - .span5 { - width: 470px; - } - .span4 { - width: 370px; - } - .span3 { - width: 270px; - } - .span2 { - width: 170px; - } - .span1 { - width: 70px; - } - .offset12 { - margin-left: 1230px; - } - .offset11 { - margin-left: 1130px; - } - .offset10 { - margin-left: 1030px; - } - .offset9 { - margin-left: 930px; - } - .offset8 { - margin-left: 830px; - } - .offset7 { - margin-left: 730px; - } - .offset6 { - margin-left: 630px; - } - .offset5 { - margin-left: 530px; - } - .offset4 { - margin-left: 430px; - } - .offset3 { - margin-left: 330px; - } - .offset2 { - margin-left: 230px; - } - .offset1 { - margin-left: 130px; - } - .row-fluid { - width: 100%; - *zoom: 1; - } - .row-fluid:before, - .row-fluid:after { - display: table; - line-height: 0; - content: ""; - } - .row-fluid:after { - clear: both; - } - .row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 30px; - margin-left: 2.564102564102564%; - *margin-left: 2.5109110747408616%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .row-fluid [class*="span"]:first-child { - margin-left: 0; - } - .row-fluid .controls-row [class*="span"] + [class*="span"] { - margin-left: 2.564102564102564%; - } - .row-fluid .span12 { - width: 100%; - *width: 99.94680851063829%; - } - .row-fluid .span11 { - width: 91.45299145299145%; - *width: 91.39979996362975%; - } - .row-fluid .span10 { - width: 82.90598290598291%; - *width: 82.8527914166212%; - } - .row-fluid .span9 { - width: 74.35897435897436%; - *width: 74.30578286961266%; - } - .row-fluid .span8 { - width: 65.81196581196582%; - *width: 65.75877432260411%; - } - .row-fluid .span7 { - width: 57.26495726495726%; - *width: 57.21176577559556%; - } - .row-fluid .span6 { - width: 48.717948717948715%; - *width: 48.664757228587014%; - } - .row-fluid .span5 { - width: 40.17094017094017%; - *width: 40.11774868157847%; - } - .row-fluid .span4 { - width: 31.623931623931625%; - *width: 31.570740134569924%; - } - .row-fluid .span3 { - width: 23.076923076923077%; - *width: 23.023731587561375%; - } - .row-fluid .span2 { - width: 14.52991452991453%; - *width: 14.476723040552828%; - } - .row-fluid .span1 { - width: 5.982905982905983%; - *width: 5.929714493544281%; - } - .row-fluid .offset12 { - margin-left: 105.12820512820512%; - *margin-left: 105.02182214948171%; - } - .row-fluid .offset12:first-child { - margin-left: 102.56410256410257%; - *margin-left: 102.45771958537915%; - } - .row-fluid .offset11 { - margin-left: 96.58119658119658%; - *margin-left: 96.47481360247316%; - } - .row-fluid .offset11:first-child { - margin-left: 94.01709401709402%; - *margin-left: 93.91071103837061%; - } - .row-fluid .offset10 { - margin-left: 88.03418803418803%; - *margin-left: 87.92780505546462%; - } - .row-fluid .offset10:first-child { - margin-left: 85.47008547008548%; - *margin-left: 85.36370249136206%; - } - .row-fluid .offset9 { - margin-left: 79.48717948717949%; - *margin-left: 79.38079650845607%; - } - .row-fluid .offset9:first-child { - margin-left: 76.92307692307693%; - *margin-left: 76.81669394435352%; - } - .row-fluid .offset8 { - margin-left: 70.94017094017094%; - *margin-left: 70.83378796144753%; - } - .row-fluid .offset8:first-child { - margin-left: 68.37606837606839%; - *margin-left: 68.26968539734497%; - } - .row-fluid .offset7 { - margin-left: 62.393162393162385%; - *margin-left: 62.28677941443899%; - } - .row-fluid .offset7:first-child { - margin-left: 59.82905982905982%; - *margin-left: 59.72267685033642%; - } - .row-fluid .offset6 { - margin-left: 53.84615384615384%; - *margin-left: 53.739770867430444%; - } - .row-fluid .offset6:first-child { - margin-left: 51.28205128205128%; - *margin-left: 51.175668303327875%; - } - .row-fluid .offset5 { - margin-left: 45.299145299145295%; - *margin-left: 45.1927623204219%; - } - .row-fluid .offset5:first-child { - margin-left: 42.73504273504273%; - *margin-left: 42.62865975631933%; - } - .row-fluid .offset4 { - margin-left: 36.75213675213675%; - *margin-left: 36.645753773413354%; - } - .row-fluid .offset4:first-child { - margin-left: 34.18803418803419%; - *margin-left: 34.081651209310785%; - } - .row-fluid .offset3 { - margin-left: 28.205128205128204%; - *margin-left: 28.0987452264048%; - } - .row-fluid .offset3:first-child { - margin-left: 25.641025641025642%; - *margin-left: 25.53464266230224%; - } - .row-fluid .offset2 { - margin-left: 19.65811965811966%; - *margin-left: 19.551736679396257%; - } - .row-fluid .offset2:first-child { - margin-left: 17.094017094017094%; - *margin-left: 16.98763411529369%; - } - .row-fluid .offset1 { - margin-left: 11.11111111111111%; - *margin-left: 11.004728132387708%; - } - .row-fluid .offset1:first-child { - margin-left: 8.547008547008547%; - *margin-left: 8.440625568285142%; - } - input, - textarea, - .uneditable-input { - margin-left: 0; - } - .controls-row [class*="span"] + [class*="span"] { - margin-left: 30px; - } - input.span12, - textarea.span12, - .uneditable-input.span12 { - width: 1156px; - } - input.span11, - textarea.span11, - .uneditable-input.span11 { - width: 1056px; - } - input.span10, - textarea.span10, - .uneditable-input.span10 { - width: 956px; - } - input.span9, - textarea.span9, - .uneditable-input.span9 { - width: 856px; - } - input.span8, - textarea.span8, - .uneditable-input.span8 { - width: 756px; - } - input.span7, - textarea.span7, - .uneditable-input.span7 { - width: 656px; - } - input.span6, - textarea.span6, - .uneditable-input.span6 { - width: 556px; - } - input.span5, - textarea.span5, - .uneditable-input.span5 { - width: 456px; - } - input.span4, - textarea.span4, - .uneditable-input.span4 { - width: 356px; - } - input.span3, - textarea.span3, - .uneditable-input.span3 { - width: 256px; - } - input.span2, - textarea.span2, - .uneditable-input.span2 { - width: 156px; - } - input.span1, - textarea.span1, - .uneditable-input.span1 { - width: 56px; - } - .thumbnails { - margin-left: -30px; - } - .thumbnails > li { - margin-left: 30px; - } - .row-fluid .thumbnails { - margin-left: 0; - } -} - -@media (min-width: 768px) and (max-width: 979px) { - .row { - margin-left: -20px; - *zoom: 1; - } - .row:before, - .row:after { - display: table; - line-height: 0; - content: ""; - } - .row:after { - clear: both; - } - [class*="span"] { - float: left; - min-height: 1px; - margin-left: 20px; - } - .container, - .navbar-static-top .container, - .navbar-fixed-top .container, - .navbar-fixed-bottom .container { - width: 724px; - } - .span12 { - width: 724px; - } - .span11 { - width: 662px; - } - .span10 { - width: 600px; - } - .span9 { - width: 538px; - } - .span8 { - width: 476px; - } - .span7 { - width: 414px; - } - .span6 { - width: 352px; - } - .span5 { - width: 290px; - } - .span4 { - width: 228px; - } - .span3 { - width: 166px; - } - .span2 { - width: 104px; - } - .span1 { - width: 42px; - } - .offset12 { - margin-left: 764px; - } - .offset11 { - margin-left: 702px; - } - .offset10 { - margin-left: 640px; - } - .offset9 { - margin-left: 578px; - } - .offset8 { - margin-left: 516px; - } - .offset7 { - margin-left: 454px; - } - .offset6 { - margin-left: 392px; - } - .offset5 { - margin-left: 330px; - } - .offset4 { - margin-left: 268px; - } - .offset3 { - margin-left: 206px; - } - .offset2 { - margin-left: 144px; - } - .offset1 { - margin-left: 82px; - } - .row-fluid { - width: 100%; - *zoom: 1; - } - .row-fluid:before, - .row-fluid:after { - display: table; - line-height: 0; - content: ""; - } - .row-fluid:after { - clear: both; - } - .row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 30px; - margin-left: 2.7624309392265194%; - *margin-left: 2.709239449864817%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .row-fluid [class*="span"]:first-child { - margin-left: 0; - } - .row-fluid .controls-row [class*="span"] + [class*="span"] { - margin-left: 2.7624309392265194%; - } - .row-fluid .span12 { - width: 100%; - *width: 99.94680851063829%; - } - .row-fluid .span11 { - width: 91.43646408839778%; - *width: 91.38327259903608%; - } - .row-fluid .span10 { - width: 82.87292817679558%; - *width: 82.81973668743387%; - } - .row-fluid .span9 { - width: 74.30939226519337%; - *width: 74.25620077583166%; - } - .row-fluid .span8 { - width: 65.74585635359117%; - *width: 65.69266486422946%; - } - .row-fluid .span7 { - width: 57.18232044198895%; - *width: 57.12912895262725%; - } - .row-fluid .span6 { - width: 48.61878453038674%; - *width: 48.56559304102504%; - } - .row-fluid .span5 { - width: 40.05524861878453%; - *width: 40.00205712942283%; - } - .row-fluid .span4 { - width: 31.491712707182323%; - *width: 31.43852121782062%; - } - .row-fluid .span3 { - width: 22.92817679558011%; - *width: 22.87498530621841%; - } - .row-fluid .span2 { - width: 14.3646408839779%; - *width: 14.311449394616199%; - } - .row-fluid .span1 { - width: 5.801104972375691%; - *width: 5.747913483013988%; - } - .row-fluid .offset12 { - margin-left: 105.52486187845304%; - *margin-left: 105.41847889972962%; - } - .row-fluid .offset12:first-child { - margin-left: 102.76243093922652%; - *margin-left: 102.6560479605031%; - } - .row-fluid .offset11 { - margin-left: 96.96132596685082%; - *margin-left: 96.8549429881274%; - } - .row-fluid .offset11:first-child { - margin-left: 94.1988950276243%; - *margin-left: 94.09251204890089%; - } - .row-fluid .offset10 { - margin-left: 88.39779005524862%; - *margin-left: 88.2914070765252%; - } - .row-fluid .offset10:first-child { - margin-left: 85.6353591160221%; - *margin-left: 85.52897613729868%; - } - .row-fluid .offset9 { - margin-left: 79.8342541436464%; - *margin-left: 79.72787116492299%; - } - .row-fluid .offset9:first-child { - margin-left: 77.07182320441989%; - *margin-left: 76.96544022569647%; - } - .row-fluid .offset8 { - margin-left: 71.2707182320442%; - *margin-left: 71.16433525332079%; - } - .row-fluid .offset8:first-child { - margin-left: 68.50828729281768%; - *margin-left: 68.40190431409427%; - } - .row-fluid .offset7 { - margin-left: 62.70718232044199%; - *margin-left: 62.600799341718584%; - } - .row-fluid .offset7:first-child { - margin-left: 59.94475138121547%; - *margin-left: 59.838368402492065%; - } - .row-fluid .offset6 { - margin-left: 54.14364640883978%; - *margin-left: 54.037263430116376%; - } - .row-fluid .offset6:first-child { - margin-left: 51.38121546961326%; - *margin-left: 51.27483249088986%; - } - .row-fluid .offset5 { - margin-left: 45.58011049723757%; - *margin-left: 45.47372751851417%; - } - .row-fluid .offset5:first-child { - margin-left: 42.81767955801105%; - *margin-left: 42.71129657928765%; - } - .row-fluid .offset4 { - margin-left: 37.01657458563536%; - *margin-left: 36.91019160691196%; - } - .row-fluid .offset4:first-child { - margin-left: 34.25414364640884%; - *margin-left: 34.14776066768544%; - } - .row-fluid .offset3 { - margin-left: 28.45303867403315%; - *margin-left: 28.346655695309746%; - } - .row-fluid .offset3:first-child { - margin-left: 25.69060773480663%; - *margin-left: 25.584224756083227%; - } - .row-fluid .offset2 { - margin-left: 19.88950276243094%; - *margin-left: 19.783119783707537%; - } - .row-fluid .offset2:first-child { - margin-left: 17.12707182320442%; - *margin-left: 17.02068884448102%; - } - .row-fluid .offset1 { - margin-left: 11.32596685082873%; - *margin-left: 11.219583872105325%; - } - .row-fluid .offset1:first-child { - margin-left: 8.56353591160221%; - *margin-left: 8.457152932878806%; - } - input, - textarea, - .uneditable-input { - margin-left: 0; - } - .controls-row [class*="span"] + [class*="span"] { - margin-left: 20px; - } - input.span12, - textarea.span12, - .uneditable-input.span12 { - width: 710px; - } - input.span11, - textarea.span11, - .uneditable-input.span11 { - width: 648px; - } - input.span10, - textarea.span10, - .uneditable-input.span10 { - width: 586px; - } - input.span9, - textarea.span9, - .uneditable-input.span9 { - width: 524px; - } - input.span8, - textarea.span8, - .uneditable-input.span8 { - width: 462px; - } - input.span7, - textarea.span7, - .uneditable-input.span7 { - width: 400px; - } - input.span6, - textarea.span6, - .uneditable-input.span6 { - width: 338px; - } - input.span5, - textarea.span5, - .uneditable-input.span5 { - width: 276px; - } - input.span4, - textarea.span4, - .uneditable-input.span4 { - width: 214px; - } - input.span3, - textarea.span3, - .uneditable-input.span3 { - width: 152px; - } - input.span2, - textarea.span2, - .uneditable-input.span2 { - width: 90px; - } - input.span1, - textarea.span1, - .uneditable-input.span1 { - width: 28px; - } -} - -@media (max-width: 767px) { - body { - padding-right: 20px; - padding-left: 20px; - } - .navbar-fixed-top, - .navbar-fixed-bottom, - .navbar-static-top { - margin-right: -20px; - margin-left: -20px; - } - .container-fluid { - padding: 0; - } - .dl-horizontal dt { - float: none; - width: auto; - clear: none; - text-align: left; - } - .dl-horizontal dd { - margin-left: 0; - } - .container { - width: auto; - } - .row-fluid { - width: 100%; - } - .row, - .thumbnails { - margin-left: 0; - } - .thumbnails > li { - float: none; - margin-left: 0; - } - [class*="span"], - .uneditable-input[class*="span"], - .row-fluid [class*="span"] { - display: block; - float: none; - width: 100%; - margin-left: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .span12, - .row-fluid .span12 { - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .row-fluid [class*="offset"]:first-child { - margin-left: 0; - } - .input-large, - .input-xlarge, - .input-xxlarge, - input[class*="span"], - select[class*="span"], - textarea[class*="span"], - .uneditable-input { - display: block; - width: 100%; - min-height: 30px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - .input-prepend input, - .input-append input, - .input-prepend input[class*="span"], - .input-append input[class*="span"] { - display: inline-block; - width: auto; - } - .controls-row [class*="span"] + [class*="span"] { - margin-left: 0; - } - .modal { - position: fixed; - top: 20px; - right: 20px; - left: 20px; - width: auto; - margin: 0; - } - .modal.fade { - top: -100px; - } - .modal.fade.in { - top: 20px; - } -} - -@media (max-width: 480px) { - .nav-collapse { - -webkit-transform: translate3d(0, 0, 0); - } - .page-header h1 small { - display: block; - line-height: 20px; - } - input[type="checkbox"], - input[type="radio"] { - border: 1px solid #ccc; - } - .form-horizontal .control-label { - float: none; - width: auto; - padding-top: 0; - text-align: left; - } - .form-horizontal .controls { - margin-left: 0; - } - .form-horizontal .control-list { - padding-top: 0; - } - .form-horizontal .form-actions { - padding-right: 10px; - padding-left: 10px; - } - .media .pull-left, - .media .pull-right { - display: block; - float: none; - margin-bottom: 10px; - } - .media-object { - margin-right: 0; - margin-left: 0; - } - .modal { - top: 10px; - right: 10px; - left: 10px; - } - .modal-header .close { - padding: 10px; - margin: -10px; - } - .carousel-caption { - position: static; - } -} - -@media (max-width: 979px) { - body { - padding-top: 0; - } - .navbar-fixed-top, - .navbar-fixed-bottom { - position: static; - } - .navbar-fixed-top { - margin-bottom: 20px; - } - .navbar-fixed-bottom { - margin-top: 20px; - } - .navbar-fixed-top .navbar-inner, - .navbar-fixed-bottom .navbar-inner { - padding: 5px; - } - .navbar .container { - width: auto; - padding: 0; - } - .navbar .brand { - padding-right: 10px; - padding-left: 10px; - margin: 0 0 0 -5px; - } - .nav-collapse { - clear: both; - } - .nav-collapse .nav { - float: none; - margin: 0 0 10px; - } - .nav-collapse .nav > li { - float: none; - } - .nav-collapse .nav > li > a { - margin-bottom: 2px; - } - .nav-collapse .nav > .divider-vertical { - display: none; - } - .nav-collapse .nav .nav-header { - color: #777777; - text-shadow: none; - } - .nav-collapse .nav > li > a, - .nav-collapse .dropdown-menu a { - padding: 9px 15px; - font-weight: bold; - color: #777777; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - } - .nav-collapse .btn { - padding: 4px 10px 4px; - font-weight: normal; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - } - .nav-collapse .dropdown-menu li + li a { - margin-bottom: 2px; - } - .nav-collapse .nav > li > a:hover, - .nav-collapse .nav > li > a:focus, - .nav-collapse .dropdown-menu a:hover, - .nav-collapse .dropdown-menu a:focus { - background-color: #f2f2f2; - } - .navbar-inverse .nav-collapse .nav > li > a, - .navbar-inverse .nav-collapse .dropdown-menu a { - color: #999999; - } - .navbar-inverse .nav-collapse .nav > li > a:hover, - .navbar-inverse .nav-collapse .nav > li > a:focus, - .navbar-inverse .nav-collapse .dropdown-menu a:hover, - .navbar-inverse .nav-collapse .dropdown-menu a:focus { - background-color: #111111; - } - .nav-collapse.in .btn-group { - padding: 0; - margin-top: 5px; - } - .nav-collapse .dropdown-menu { - position: static; - top: auto; - left: auto; - display: none; - float: none; - max-width: none; - padding: 0; - margin: 0 15px; - background-color: transparent; - border: none; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - } - .nav-collapse .open > .dropdown-menu { - display: block; - } - .nav-collapse .dropdown-menu:before, - .nav-collapse .dropdown-menu:after { - display: none; - } - .nav-collapse .dropdown-menu .divider { - display: none; - } - .nav-collapse .nav > li > .dropdown-menu:before, - .nav-collapse .nav > li > .dropdown-menu:after { - display: none; - } - .nav-collapse .navbar-form, - .nav-collapse .navbar-search { - float: none; - padding: 10px 15px; - margin: 10px 0; - border-top: 1px solid #f2f2f2; - border-bottom: 1px solid #f2f2f2; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - } - .navbar-inverse .nav-collapse .navbar-form, - .navbar-inverse .nav-collapse .navbar-search { - border-top-color: #111111; - border-bottom-color: #111111; - } - .navbar .nav-collapse .nav.pull-right { - float: none; - margin-left: 0; - } - .nav-collapse, - .nav-collapse.collapse { - height: 0; - overflow: hidden; - } - .navbar .btn-navbar { - display: block; - } - .navbar-static .navbar-inner { - padding-right: 10px; - padding-left: 10px; - } -} - -@media (min-width: 980px) { - .nav-collapse.collapse { - height: auto !important; - overflow: visible !important; - } -} diff --git a/gae/webapp/static/bootstrap/css/bootstrap-responsive.min.css b/gae/webapp/static/bootstrap/css/bootstrap-responsive.min.css deleted file mode 100644 index d1b7f4b..0000000 --- a/gae/webapp/static/bootstrap/css/bootstrap-responsive.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap Responsive v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/gae/webapp/static/bootstrap/css/bootstrap.css b/gae/webapp/static/bootstrap/css/bootstrap.css deleted file mode 100644 index 2f56af3..0000000 --- a/gae/webapp/static/bootstrap/css/bootstrap.css +++ /dev/null @@ -1,6158 +0,0 @@ -/*! - * Bootstrap v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ - -.clearfix { - *zoom: 1; -} - -.clearfix:before, -.clearfix:after { - display: table; - line-height: 0; - content: ""; -} - -.clearfix:after { - clear: both; -} - -.hide-text { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -.input-block-level { - display: block; - width: 100%; - min-height: 30px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section { - display: block; -} - -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -audio:not([controls]) { - display: none; -} - -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} - -a:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -a:hover, -a:active { - outline: 0; -} - -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -img { - width: auto\9; - height: auto; - max-width: 100%; - vertical-align: middle; - border: 0; - -ms-interpolation-mode: bicubic; -} - -#map_canvas img, -.google-maps img { - max-width: none; -} - -button, -input, -select, -textarea { - margin: 0; - font-size: 100%; - vertical-align: middle; -} - -button, -input { - *overflow: visible; - line-height: normal; -} - -button::-moz-focus-inner, -input::-moz-focus-inner { - padding: 0; - border: 0; -} - -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - -webkit-appearance: button; -} - -label, -select, -button, -input[type="button"], -input[type="reset"], -input[type="submit"], -input[type="radio"], -input[type="checkbox"] { - cursor: pointer; -} - -input[type="search"] { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; -} - -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} - -textarea { - overflow: auto; - vertical-align: top; -} - -@media print { - * { - color: #000 !important; - text-shadow: none !important; - background: transparent !important; - box-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - @page { - margin: 0.5cm; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } -} - -body { - margin: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 20px; - color: #333333; - background-color: #ffffff; -} - -a { - color: #0088cc; - text-decoration: none; -} - -a:hover, -a:focus { - color: #005580; - text-decoration: underline; -} - -.img-rounded { - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.img-polaroid { - padding: 4px; - background-color: #fff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.img-circle { - -webkit-border-radius: 500px; - -moz-border-radius: 500px; - border-radius: 500px; -} - -.row { - margin-left: -20px; - *zoom: 1; -} - -.row:before, -.row:after { - display: table; - line-height: 0; - content: ""; -} - -.row:after { - clear: both; -} - -[class*="span"] { - float: left; - min-height: 1px; - margin-left: 20px; -} - -.container, -.navbar-static-top .container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.span12 { - width: 940px; -} - -.span11 { - width: 860px; -} - -.span10 { - width: 780px; -} - -.span9 { - width: 700px; -} - -.span8 { - width: 620px; -} - -.span7 { - width: 540px; -} - -.span6 { - width: 460px; -} - -.span5 { - width: 380px; -} - -.span4 { - width: 300px; -} - -.span3 { - width: 220px; -} - -.span2 { - width: 140px; -} - -.span1 { - width: 60px; -} - -.offset12 { - margin-left: 980px; -} - -.offset11 { - margin-left: 900px; -} - -.offset10 { - margin-left: 820px; -} - -.offset9 { - margin-left: 740px; -} - -.offset8 { - margin-left: 660px; -} - -.offset7 { - margin-left: 580px; -} - -.offset6 { - margin-left: 500px; -} - -.offset5 { - margin-left: 420px; -} - -.offset4 { - margin-left: 340px; -} - -.offset3 { - margin-left: 260px; -} - -.offset2 { - margin-left: 180px; -} - -.offset1 { - margin-left: 100px; -} - -.row-fluid { - width: 100%; - *zoom: 1; -} - -.row-fluid:before, -.row-fluid:after { - display: table; - line-height: 0; - content: ""; -} - -.row-fluid:after { - clear: both; -} - -.row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 30px; - margin-left: 2.127659574468085%; - *margin-left: 2.074468085106383%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.row-fluid [class*="span"]:first-child { - margin-left: 0; -} - -.row-fluid .controls-row [class*="span"] + [class*="span"] { - margin-left: 2.127659574468085%; -} - -.row-fluid .span12 { - width: 100%; - *width: 99.94680851063829%; -} - -.row-fluid .span11 { - width: 91.48936170212765%; - *width: 91.43617021276594%; -} - -.row-fluid .span10 { - width: 82.97872340425532%; - *width: 82.92553191489361%; -} - -.row-fluid .span9 { - width: 74.46808510638297%; - *width: 74.41489361702126%; -} - -.row-fluid .span8 { - width: 65.95744680851064%; - *width: 65.90425531914893%; -} - -.row-fluid .span7 { - width: 57.44680851063829%; - *width: 57.39361702127659%; -} - -.row-fluid .span6 { - width: 48.93617021276595%; - *width: 48.88297872340425%; -} - -.row-fluid .span5 { - width: 40.42553191489362%; - *width: 40.37234042553192%; -} - -.row-fluid .span4 { - width: 31.914893617021278%; - *width: 31.861702127659576%; -} - -.row-fluid .span3 { - width: 23.404255319148934%; - *width: 23.351063829787233%; -} - -.row-fluid .span2 { - width: 14.893617021276595%; - *width: 14.840425531914894%; -} - -.row-fluid .span1 { - width: 6.382978723404255%; - *width: 6.329787234042553%; -} - -.row-fluid .offset12 { - margin-left: 104.25531914893617%; - *margin-left: 104.14893617021275%; -} - -.row-fluid .offset12:first-child { - margin-left: 102.12765957446808%; - *margin-left: 102.02127659574467%; -} - -.row-fluid .offset11 { - margin-left: 95.74468085106382%; - *margin-left: 95.6382978723404%; -} - -.row-fluid .offset11:first-child { - margin-left: 93.61702127659574%; - *margin-left: 93.51063829787232%; -} - -.row-fluid .offset10 { - margin-left: 87.23404255319149%; - *margin-left: 87.12765957446807%; -} - -.row-fluid .offset10:first-child { - margin-left: 85.1063829787234%; - *margin-left: 84.99999999999999%; -} - -.row-fluid .offset9 { - margin-left: 78.72340425531914%; - *margin-left: 78.61702127659572%; -} - -.row-fluid .offset9:first-child { - margin-left: 76.59574468085106%; - *margin-left: 76.48936170212764%; -} - -.row-fluid .offset8 { - margin-left: 70.2127659574468%; - *margin-left: 70.10638297872339%; -} - -.row-fluid .offset8:first-child { - margin-left: 68.08510638297872%; - *margin-left: 67.9787234042553%; -} - -.row-fluid .offset7 { - margin-left: 61.70212765957446%; - *margin-left: 61.59574468085106%; -} - -.row-fluid .offset7:first-child { - margin-left: 59.574468085106375%; - *margin-left: 59.46808510638297%; -} - -.row-fluid .offset6 { - margin-left: 53.191489361702125%; - *margin-left: 53.085106382978715%; -} - -.row-fluid .offset6:first-child { - margin-left: 51.063829787234035%; - *margin-left: 50.95744680851063%; -} - -.row-fluid .offset5 { - margin-left: 44.68085106382979%; - *margin-left: 44.57446808510638%; -} - -.row-fluid .offset5:first-child { - margin-left: 42.5531914893617%; - *margin-left: 42.4468085106383%; -} - -.row-fluid .offset4 { - margin-left: 36.170212765957444%; - *margin-left: 36.06382978723405%; -} - -.row-fluid .offset4:first-child { - margin-left: 34.04255319148936%; - *margin-left: 33.93617021276596%; -} - -.row-fluid .offset3 { - margin-left: 27.659574468085104%; - *margin-left: 27.5531914893617%; -} - -.row-fluid .offset3:first-child { - margin-left: 25.53191489361702%; - *margin-left: 25.425531914893618%; -} - -.row-fluid .offset2 { - margin-left: 19.148936170212764%; - *margin-left: 19.04255319148936%; -} - -.row-fluid .offset2:first-child { - margin-left: 17.02127659574468%; - *margin-left: 16.914893617021278%; -} - -.row-fluid .offset1 { - margin-left: 10.638297872340425%; - *margin-left: 10.53191489361702%; -} - -.row-fluid .offset1:first-child { - margin-left: 8.51063829787234%; - *margin-left: 8.404255319148938%; -} - -[class*="span"].hide, -.row-fluid [class*="span"].hide { - display: none; -} - -[class*="span"].pull-right, -.row-fluid [class*="span"].pull-right { - float: right; -} - -.container { - margin-right: auto; - margin-left: auto; - *zoom: 1; -} - -.container:before, -.container:after { - display: table; - line-height: 0; - content: ""; -} - -.container:after { - clear: both; -} - -.container-fluid { - padding-right: 20px; - padding-left: 20px; - *zoom: 1; -} - -.container-fluid:before, -.container-fluid:after { - display: table; - line-height: 0; - content: ""; -} - -.container-fluid:after { - clear: both; -} - -p { - margin: 0 0 10px; -} - -.lead { - margin-bottom: 20px; - font-size: 21px; - font-weight: 200; - line-height: 30px; -} - -small { - font-size: 85%; -} - -strong { - font-weight: bold; -} - -em { - font-style: italic; -} - -cite { - font-style: normal; -} - -.muted { - color: #999999; -} - -a.muted:hover, -a.muted:focus { - color: #808080; -} - -.text-warning { - color: #c09853; -} - -a.text-warning:hover, -a.text-warning:focus { - color: #a47e3c; -} - -.text-error { - color: #b94a48; -} - -a.text-error:hover, -a.text-error:focus { - color: #953b39; -} - -.text-info { - color: #3a87ad; -} - -a.text-info:hover, -a.text-info:focus { - color: #2d6987; -} - -.text-success { - color: #468847; -} - -a.text-success:hover, -a.text-success:focus { - color: #356635; -} - -.text-left { - text-align: left; -} - -.text-right { - text-align: right; -} - -.text-center { - text-align: center; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin: 10px 0; - font-family: inherit; - font-weight: bold; - line-height: 20px; - color: inherit; - text-rendering: optimizelegibility; -} - -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small { - font-weight: normal; - line-height: 1; - color: #999999; -} - -h1, -h2, -h3 { - line-height: 40px; -} - -h1 { - font-size: 38.5px; -} - -h2 { - font-size: 31.5px; -} - -h3 { - font-size: 24.5px; -} - -h4 { - font-size: 17.5px; -} - -h5 { - font-size: 14px; -} - -h6 { - font-size: 11.9px; -} - -h1 small { - font-size: 24.5px; -} - -h2 small { - font-size: 17.5px; -} - -h3 small { - font-size: 14px; -} - -h4 small { - font-size: 14px; -} - -.page-header { - padding-bottom: 9px; - margin: 20px 0 30px; - border-bottom: 1px solid #eeeeee; -} - -ul, -ol { - padding: 0; - margin: 0 0 10px 25px; -} - -ul ul, -ul ol, -ol ol, -ol ul { - margin-bottom: 0; -} - -li { - line-height: 20px; -} - -ul.unstyled, -ol.unstyled { - margin-left: 0; - list-style: none; -} - -ul.inline, -ol.inline { - margin-left: 0; - list-style: none; -} - -ul.inline > li, -ol.inline > li { - display: inline-block; - *display: inline; - padding-right: 5px; - padding-left: 5px; - *zoom: 1; -} - -dl { - margin-bottom: 20px; -} - -dt, -dd { - line-height: 20px; -} - -dt { - font-weight: bold; -} - -dd { - margin-left: 10px; -} - -.dl-horizontal { - *zoom: 1; -} - -.dl-horizontal:before, -.dl-horizontal:after { - display: table; - line-height: 0; - content: ""; -} - -.dl-horizontal:after { - clear: both; -} - -.dl-horizontal dt { - float: left; - width: 160px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; -} - -.dl-horizontal dd { - margin-left: 180px; -} - -hr { - margin: 20px 0; - border: 0; - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #ffffff; -} - -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #999999; -} - -abbr.initialism { - font-size: 90%; - text-transform: uppercase; -} - -blockquote { - padding: 0 0 0 15px; - margin: 0 0 20px; - border-left: 5px solid #eeeeee; -} - -blockquote p { - margin-bottom: 0; - font-size: 17.5px; - font-weight: 300; - line-height: 1.25; -} - -blockquote small { - display: block; - line-height: 20px; - color: #999999; -} - -blockquote small:before { - content: '\2014 \00A0'; -} - -blockquote.pull-right { - float: right; - padding-right: 15px; - padding-left: 0; - border-right: 5px solid #eeeeee; - border-left: 0; -} - -blockquote.pull-right p, -blockquote.pull-right small { - text-align: right; -} - -blockquote.pull-right small:before { - content: ''; -} - -blockquote.pull-right small:after { - content: '\00A0 \2014'; -} - -q:before, -q:after, -blockquote:before, -blockquote:after { - content: ""; -} - -address { - display: block; - margin-bottom: 20px; - font-style: normal; - line-height: 20px; -} - -code, -pre { - padding: 0 3px 2px; - font-family: Monaco, Menlo, Consolas, "Courier New", monospace; - font-size: 12px; - color: #333333; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -code { - padding: 2px 4px; - color: #d14; - white-space: nowrap; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; -} - -pre { - display: block; - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - line-height: 20px; - word-break: break-all; - word-wrap: break-word; - white-space: pre; - white-space: pre-wrap; - background-color: #f5f5f5; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.15); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -pre.prettyprint { - margin-bottom: 20px; -} - -pre code { - padding: 0; - color: inherit; - white-space: pre; - white-space: pre-wrap; - background-color: transparent; - border: 0; -} - -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} - -form { - margin: 0 0 20px; -} - -fieldset { - padding: 0; - margin: 0; - border: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 20px; - font-size: 21px; - line-height: 40px; - color: #333333; - border: 0; - border-bottom: 1px solid #e5e5e5; -} - -legend small { - font-size: 15px; - color: #999999; -} - -label, -input, -button, -select, -textarea { - font-size: 14px; - font-weight: normal; - line-height: 20px; -} - -input, -button, -select, -textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -label { - display: block; - margin-bottom: 5px; -} - -select, -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - display: inline-block; - height: 20px; - padding: 4px 6px; - margin-bottom: 10px; - font-size: 14px; - line-height: 20px; - color: #555555; - vertical-align: middle; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -input, -textarea, -.uneditable-input { - width: 206px; -} - -textarea { - height: auto; -} - -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - background-color: #ffffff; - border: 1px solid #cccccc; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - -o-transition: border linear 0.2s, box-shadow linear 0.2s; - transition: border linear 0.2s, box-shadow linear 0.2s; -} - -textarea:focus, -input[type="text"]:focus, -input[type="password"]:focus, -input[type="datetime"]:focus, -input[type="datetime-local"]:focus, -input[type="date"]:focus, -input[type="month"]:focus, -input[type="time"]:focus, -input[type="week"]:focus, -input[type="number"]:focus, -input[type="email"]:focus, -input[type="url"]:focus, -input[type="search"]:focus, -input[type="tel"]:focus, -input[type="color"]:focus, -.uneditable-input:focus { - border-color: rgba(82, 168, 236, 0.8); - outline: 0; - outline: thin dotted \9; - /* IE6-9 */ - - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); -} - -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - *margin-top: 0; - line-height: normal; -} - -input[type="file"], -input[type="image"], -input[type="submit"], -input[type="reset"], -input[type="button"], -input[type="radio"], -input[type="checkbox"] { - width: auto; -} - -select, -input[type="file"] { - height: 30px; - /* In IE7, the height of the select element cannot be changed by height, only font-size */ - - *margin-top: 4px; - /* For IE7, add top margin to align select with labels */ - - line-height: 30px; -} - -select { - width: 220px; - background-color: #ffffff; - border: 1px solid #cccccc; -} - -select[multiple], -select[size] { - height: auto; -} - -select:focus, -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -.uneditable-input, -.uneditable-textarea { - color: #999999; - cursor: not-allowed; - background-color: #fcfcfc; - border-color: #cccccc; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); -} - -.uneditable-input { - overflow: hidden; - white-space: nowrap; -} - -.uneditable-textarea { - width: auto; - height: auto; -} - -input:-moz-placeholder, -textarea:-moz-placeholder { - color: #999999; -} - -input:-ms-input-placeholder, -textarea:-ms-input-placeholder { - color: #999999; -} - -input::-webkit-input-placeholder, -textarea::-webkit-input-placeholder { - color: #999999; -} - -.radio, -.checkbox { - min-height: 20px; - padding-left: 20px; -} - -.radio input[type="radio"], -.checkbox input[type="checkbox"] { - float: left; - margin-left: -20px; -} - -.controls > .radio:first-child, -.controls > .checkbox:first-child { - padding-top: 5px; -} - -.radio.inline, -.checkbox.inline { - display: inline-block; - padding-top: 5px; - margin-bottom: 0; - vertical-align: middle; -} - -.radio.inline + .radio.inline, -.checkbox.inline + .checkbox.inline { - margin-left: 10px; -} - -.input-mini { - width: 60px; -} - -.input-small { - width: 90px; -} - -.input-medium { - width: 150px; -} - -.input-large { - width: 210px; -} - -.input-xlarge { - width: 270px; -} - -.input-xxlarge { - width: 530px; -} - -input[class*="span"], -select[class*="span"], -textarea[class*="span"], -.uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"] { - float: none; - margin-left: 0; -} - -.input-append input[class*="span"], -.input-append .uneditable-input[class*="span"], -.input-prepend input[class*="span"], -.input-prepend .uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"], -.row-fluid .input-prepend [class*="span"], -.row-fluid .input-append [class*="span"] { - display: inline-block; -} - -input, -textarea, -.uneditable-input { - margin-left: 0; -} - -.controls-row [class*="span"] + [class*="span"] { - margin-left: 20px; -} - -input.span12, -textarea.span12, -.uneditable-input.span12 { - width: 926px; -} - -input.span11, -textarea.span11, -.uneditable-input.span11 { - width: 846px; -} - -input.span10, -textarea.span10, -.uneditable-input.span10 { - width: 766px; -} - -input.span9, -textarea.span9, -.uneditable-input.span9 { - width: 686px; -} - -input.span8, -textarea.span8, -.uneditable-input.span8 { - width: 606px; -} - -input.span7, -textarea.span7, -.uneditable-input.span7 { - width: 526px; -} - -input.span6, -textarea.span6, -.uneditable-input.span6 { - width: 446px; -} - -input.span5, -textarea.span5, -.uneditable-input.span5 { - width: 366px; -} - -input.span4, -textarea.span4, -.uneditable-input.span4 { - width: 286px; -} - -input.span3, -textarea.span3, -.uneditable-input.span3 { - width: 206px; -} - -input.span2, -textarea.span2, -.uneditable-input.span2 { - width: 126px; -} - -input.span1, -textarea.span1, -.uneditable-input.span1 { - width: 46px; -} - -.controls-row { - *zoom: 1; -} - -.controls-row:before, -.controls-row:after { - display: table; - line-height: 0; - content: ""; -} - -.controls-row:after { - clear: both; -} - -.controls-row [class*="span"], -.row-fluid .controls-row [class*="span"] { - float: left; -} - -.controls-row .checkbox[class*="span"], -.controls-row .radio[class*="span"] { - padding-top: 5px; -} - -input[disabled], -select[disabled], -textarea[disabled], -input[readonly], -select[readonly], -textarea[readonly] { - cursor: not-allowed; - background-color: #eeeeee; -} - -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"][readonly], -input[type="checkbox"][readonly] { - background-color: transparent; -} - -.control-group.warning .control-label, -.control-group.warning .help-block, -.control-group.warning .help-inline { - color: #c09853; -} - -.control-group.warning .checkbox, -.control-group.warning .radio, -.control-group.warning input, -.control-group.warning select, -.control-group.warning textarea { - color: #c09853; -} - -.control-group.warning input, -.control-group.warning select, -.control-group.warning textarea { - border-color: #c09853; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.warning input:focus, -.control-group.warning select:focus, -.control-group.warning textarea:focus { - border-color: #a47e3c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; -} - -.control-group.warning .input-prepend .add-on, -.control-group.warning .input-append .add-on { - color: #c09853; - background-color: #fcf8e3; - border-color: #c09853; -} - -.control-group.error .control-label, -.control-group.error .help-block, -.control-group.error .help-inline { - color: #b94a48; -} - -.control-group.error .checkbox, -.control-group.error .radio, -.control-group.error input, -.control-group.error select, -.control-group.error textarea { - color: #b94a48; -} - -.control-group.error input, -.control-group.error select, -.control-group.error textarea { - border-color: #b94a48; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.error input:focus, -.control-group.error select:focus, -.control-group.error textarea:focus { - border-color: #953b39; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; -} - -.control-group.error .input-prepend .add-on, -.control-group.error .input-append .add-on { - color: #b94a48; - background-color: #f2dede; - border-color: #b94a48; -} - -.control-group.success .control-label, -.control-group.success .help-block, -.control-group.success .help-inline { - color: #468847; -} - -.control-group.success .checkbox, -.control-group.success .radio, -.control-group.success input, -.control-group.success select, -.control-group.success textarea { - color: #468847; -} - -.control-group.success input, -.control-group.success select, -.control-group.success textarea { - border-color: #468847; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.success input:focus, -.control-group.success select:focus, -.control-group.success textarea:focus { - border-color: #356635; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; -} - -.control-group.success .input-prepend .add-on, -.control-group.success .input-append .add-on { - color: #468847; - background-color: #dff0d8; - border-color: #468847; -} - -.control-group.info .control-label, -.control-group.info .help-block, -.control-group.info .help-inline { - color: #3a87ad; -} - -.control-group.info .checkbox, -.control-group.info .radio, -.control-group.info input, -.control-group.info select, -.control-group.info textarea { - color: #3a87ad; -} - -.control-group.info input, -.control-group.info select, -.control-group.info textarea { - border-color: #3a87ad; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.info input:focus, -.control-group.info select:focus, -.control-group.info textarea:focus { - border-color: #2d6987; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; -} - -.control-group.info .input-prepend .add-on, -.control-group.info .input-append .add-on { - color: #3a87ad; - background-color: #d9edf7; - border-color: #3a87ad; -} - -input:focus:invalid, -textarea:focus:invalid, -select:focus:invalid { - color: #b94a48; - border-color: #ee5f5b; -} - -input:focus:invalid:focus, -textarea:focus:invalid:focus, -select:focus:invalid:focus { - border-color: #e9322d; - -webkit-box-shadow: 0 0 6px #f8b9b7; - -moz-box-shadow: 0 0 6px #f8b9b7; - box-shadow: 0 0 6px #f8b9b7; -} - -.form-actions { - padding: 19px 20px 20px; - margin-top: 20px; - margin-bottom: 20px; - background-color: #f5f5f5; - border-top: 1px solid #e5e5e5; - *zoom: 1; -} - -.form-actions:before, -.form-actions:after { - display: table; - line-height: 0; - content: ""; -} - -.form-actions:after { - clear: both; -} - -.help-block, -.help-inline { - color: #595959; -} - -.help-block { - display: block; - margin-bottom: 10px; -} - -.help-inline { - display: inline-block; - *display: inline; - padding-left: 5px; - vertical-align: middle; - *zoom: 1; -} - -.input-append, -.input-prepend { - display: inline-block; - margin-bottom: 10px; - font-size: 0; - white-space: nowrap; - vertical-align: middle; -} - -.input-append input, -.input-prepend input, -.input-append select, -.input-prepend select, -.input-append .uneditable-input, -.input-prepend .uneditable-input, -.input-append .dropdown-menu, -.input-prepend .dropdown-menu, -.input-append .popover, -.input-prepend .popover { - font-size: 14px; -} - -.input-append input, -.input-prepend input, -.input-append select, -.input-prepend select, -.input-append .uneditable-input, -.input-prepend .uneditable-input { - position: relative; - margin-bottom: 0; - *margin-left: 0; - vertical-align: top; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-append input:focus, -.input-prepend input:focus, -.input-append select:focus, -.input-prepend select:focus, -.input-append .uneditable-input:focus, -.input-prepend .uneditable-input:focus { - z-index: 2; -} - -.input-append .add-on, -.input-prepend .add-on { - display: inline-block; - width: auto; - height: 20px; - min-width: 16px; - padding: 4px 5px; - font-size: 14px; - font-weight: normal; - line-height: 20px; - text-align: center; - text-shadow: 0 1px 0 #ffffff; - background-color: #eeeeee; - border: 1px solid #ccc; -} - -.input-append .add-on, -.input-prepend .add-on, -.input-append .btn, -.input-prepend .btn, -.input-append .btn-group > .dropdown-toggle, -.input-prepend .btn-group > .dropdown-toggle { - vertical-align: top; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-append .active, -.input-prepend .active { - background-color: #a9dba9; - border-color: #46a546; -} - -.input-prepend .add-on, -.input-prepend .btn { - margin-right: -1px; -} - -.input-prepend .add-on:first-child, -.input-prepend .btn:first-child { - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-append input, -.input-append select, -.input-append .uneditable-input { - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-append input + .btn-group .btn:last-child, -.input-append select + .btn-group .btn:last-child, -.input-append .uneditable-input + .btn-group .btn:last-child { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-append .add-on, -.input-append .btn, -.input-append .btn-group { - margin-left: -1px; -} - -.input-append .add-on:last-child, -.input-append .btn:last-child, -.input-append .btn-group:last-child > .dropdown-toggle { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append input, -.input-prepend.input-append select, -.input-prepend.input-append .uneditable-input { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-prepend.input-append input + .btn-group .btn, -.input-prepend.input-append select + .btn-group .btn, -.input-prepend.input-append .uneditable-input + .btn-group .btn { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append .add-on:first-child, -.input-prepend.input-append .btn:first-child { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-prepend.input-append .add-on:last-child, -.input-prepend.input-append .btn:last-child { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append .btn-group:first-child { - margin-left: 0; -} - -input.search-query { - padding-right: 14px; - padding-right: 4px \9; - padding-left: 14px; - padding-left: 4px \9; - /* IE7-8 doesn't have border-radius, so don't indent the padding */ - - margin-bottom: 0; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -/* Allow for input prepend/append in search forms */ - -.form-search .input-append .search-query, -.form-search .input-prepend .search-query { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.form-search .input-append .search-query { - -webkit-border-radius: 14px 0 0 14px; - -moz-border-radius: 14px 0 0 14px; - border-radius: 14px 0 0 14px; -} - -.form-search .input-append .btn { - -webkit-border-radius: 0 14px 14px 0; - -moz-border-radius: 0 14px 14px 0; - border-radius: 0 14px 14px 0; -} - -.form-search .input-prepend .search-query { - -webkit-border-radius: 0 14px 14px 0; - -moz-border-radius: 0 14px 14px 0; - border-radius: 0 14px 14px 0; -} - -.form-search .input-prepend .btn { - -webkit-border-radius: 14px 0 0 14px; - -moz-border-radius: 14px 0 0 14px; - border-radius: 14px 0 0 14px; -} - -.form-search input, -.form-inline input, -.form-horizontal input, -.form-search textarea, -.form-inline textarea, -.form-horizontal textarea, -.form-search select, -.form-inline select, -.form-horizontal select, -.form-search .help-inline, -.form-inline .help-inline, -.form-horizontal .help-inline, -.form-search .uneditable-input, -.form-inline .uneditable-input, -.form-horizontal .uneditable-input, -.form-search .input-prepend, -.form-inline .input-prepend, -.form-horizontal .input-prepend, -.form-search .input-append, -.form-inline .input-append, -.form-horizontal .input-append { - display: inline-block; - *display: inline; - margin-bottom: 0; - vertical-align: middle; - *zoom: 1; -} - -.form-search .hide, -.form-inline .hide, -.form-horizontal .hide { - display: none; -} - -.form-search label, -.form-inline label, -.form-search .btn-group, -.form-inline .btn-group { - display: inline-block; -} - -.form-search .input-append, -.form-inline .input-append, -.form-search .input-prepend, -.form-inline .input-prepend { - margin-bottom: 0; -} - -.form-search .radio, -.form-search .checkbox, -.form-inline .radio, -.form-inline .checkbox { - padding-left: 0; - margin-bottom: 0; - vertical-align: middle; -} - -.form-search .radio input[type="radio"], -.form-search .checkbox input[type="checkbox"], -.form-inline .radio input[type="radio"], -.form-inline .checkbox input[type="checkbox"] { - float: left; - margin-right: 3px; - margin-left: 0; -} - -.control-group { - margin-bottom: 10px; -} - -legend + .control-group { - margin-top: 20px; - -webkit-margin-top-collapse: separate; -} - -.form-horizontal .control-group { - margin-bottom: 20px; - *zoom: 1; -} - -.form-horizontal .control-group:before, -.form-horizontal .control-group:after { - display: table; - line-height: 0; - content: ""; -} - -.form-horizontal .control-group:after { - clear: both; -} - -.form-horizontal .control-label { - float: left; - width: 160px; - padding-top: 5px; - text-align: right; -} - -.form-horizontal .controls { - *display: inline-block; - *padding-left: 20px; - margin-left: 180px; - *margin-left: 0; -} - -.form-horizontal .controls:first-child { - *padding-left: 180px; -} - -.form-horizontal .help-block { - margin-bottom: 0; -} - -.form-horizontal input + .help-block, -.form-horizontal select + .help-block, -.form-horizontal textarea + .help-block, -.form-horizontal .uneditable-input + .help-block, -.form-horizontal .input-prepend + .help-block, -.form-horizontal .input-append + .help-block { - margin-top: 10px; -} - -.form-horizontal .form-actions { - padding-left: 180px; -} - -table { - max-width: 100%; - background-color: transparent; - border-collapse: collapse; - border-spacing: 0; -} - -.table { - width: 100%; - margin-bottom: 20px; -} - -.table th, -.table td { - padding: 8px; - line-height: 20px; - text-align: left; - vertical-align: top; - border-top: 1px solid #dddddd; -} - -.table th { - font-weight: bold; -} - -.table thead th { - vertical-align: bottom; -} - -.table caption + thead tr:first-child th, -.table caption + thead tr:first-child td, -.table colgroup + thead tr:first-child th, -.table colgroup + thead tr:first-child td, -.table thead:first-child tr:first-child th, -.table thead:first-child tr:first-child td { - border-top: 0; -} - -.table tbody + tbody { - border-top: 2px solid #dddddd; -} - -.table .table { - background-color: #ffffff; -} - -.table-condensed th, -.table-condensed td { - padding: 4px 5px; -} - -.table-bordered { - border: 1px solid #dddddd; - border-collapse: separate; - *border-collapse: collapse; - border-left: 0; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.table-bordered th, -.table-bordered td { - border-left: 1px solid #dddddd; -} - -.table-bordered caption + thead tr:first-child th, -.table-bordered caption + tbody tr:first-child th, -.table-bordered caption + tbody tr:first-child td, -.table-bordered colgroup + thead tr:first-child th, -.table-bordered colgroup + tbody tr:first-child th, -.table-bordered colgroup + tbody tr:first-child td, -.table-bordered thead:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child td { - border-top: 0; -} - -.table-bordered thead:first-child tr:first-child > th:first-child, -.table-bordered tbody:first-child tr:first-child > td:first-child, -.table-bordered tbody:first-child tr:first-child > th:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; -} - -.table-bordered thead:first-child tr:first-child > th:last-child, -.table-bordered tbody:first-child tr:first-child > td:last-child, -.table-bordered tbody:first-child tr:first-child > th:last-child { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; -} - -.table-bordered thead:last-child tr:last-child > th:first-child, -.table-bordered tbody:last-child tr:last-child > td:first-child, -.table-bordered tbody:last-child tr:last-child > th:first-child, -.table-bordered tfoot:last-child tr:last-child > td:first-child, -.table-bordered tfoot:last-child tr:last-child > th:first-child { - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; -} - -.table-bordered thead:last-child tr:last-child > th:last-child, -.table-bordered tbody:last-child tr:last-child > td:last-child, -.table-bordered tbody:last-child tr:last-child > th:last-child, -.table-bordered tfoot:last-child tr:last-child > td:last-child, -.table-bordered tfoot:last-child tr:last-child > th:last-child { - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-bottomright: 4px; -} - -.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { - -webkit-border-bottom-left-radius: 0; - border-bottom-left-radius: 0; - -moz-border-radius-bottomleft: 0; -} - -.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { - -webkit-border-bottom-right-radius: 0; - border-bottom-right-radius: 0; - -moz-border-radius-bottomright: 0; -} - -.table-bordered caption + thead tr:first-child th:first-child, -.table-bordered caption + tbody tr:first-child td:first-child, -.table-bordered colgroup + thead tr:first-child th:first-child, -.table-bordered colgroup + tbody tr:first-child td:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; -} - -.table-bordered caption + thead tr:first-child th:last-child, -.table-bordered caption + tbody tr:first-child td:last-child, -.table-bordered colgroup + thead tr:first-child th:last-child, -.table-bordered colgroup + tbody tr:first-child td:last-child { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; -} - -.table-striped tbody > tr:nth-child(odd) > td, -.table-striped tbody > tr:nth-child(odd) > th { - background-color: #f9f9f9; -} - -.table-hover tbody tr:hover > td, -.table-hover tbody tr:hover > th { - background-color: #f5f5f5; -} - -table td[class*="span"], -table th[class*="span"], -.row-fluid table td[class*="span"], -.row-fluid table th[class*="span"] { - display: table-cell; - float: none; - margin-left: 0; -} - -.table td.span1, -.table th.span1 { - float: none; - width: 44px; - margin-left: 0; -} - -.table td.span2, -.table th.span2 { - float: none; - width: 124px; - margin-left: 0; -} - -.table td.span3, -.table th.span3 { - float: none; - width: 204px; - margin-left: 0; -} - -.table td.span4, -.table th.span4 { - float: none; - width: 284px; - margin-left: 0; -} - -.table td.span5, -.table th.span5 { - float: none; - width: 364px; - margin-left: 0; -} - -.table td.span6, -.table th.span6 { - float: none; - width: 444px; - margin-left: 0; -} - -.table td.span7, -.table th.span7 { - float: none; - width: 524px; - margin-left: 0; -} - -.table td.span8, -.table th.span8 { - float: none; - width: 604px; - margin-left: 0; -} - -.table td.span9, -.table th.span9 { - float: none; - width: 684px; - margin-left: 0; -} - -.table td.span10, -.table th.span10 { - float: none; - width: 764px; - margin-left: 0; -} - -.table td.span11, -.table th.span11 { - float: none; - width: 844px; - margin-left: 0; -} - -.table td.span12, -.table th.span12 { - float: none; - width: 924px; - margin-left: 0; -} - -.table tbody tr.success > td { - background-color: #dff0d8; -} - -.table tbody tr.error > td { - background-color: #f2dede; -} - -.table tbody tr.warning > td { - background-color: #fcf8e3; -} - -.table tbody tr.info > td { - background-color: #d9edf7; -} - -.table-hover tbody tr.success:hover > td { - background-color: #d0e9c6; -} - -.table-hover tbody tr.error:hover > td { - background-color: #ebcccc; -} - -.table-hover tbody tr.warning:hover > td { - background-color: #faf2cc; -} - -.table-hover tbody tr.info:hover > td { - background-color: #c4e3f3; -} - -[class^="icon-"], -[class*=" icon-"] { - display: inline-block; - width: 14px; - height: 14px; - margin-top: 1px; - *margin-right: .3em; - line-height: 14px; - vertical-align: text-top; - background-image: url("../img/glyphicons-halflings.png"); - background-position: 14px 14px; - background-repeat: no-repeat; -} - -/* White icons with optional class, or on hover/focus/active states of certain elements */ - -.icon-white, -.nav-pills > .active > a > [class^="icon-"], -.nav-pills > .active > a > [class*=" icon-"], -.nav-list > .active > a > [class^="icon-"], -.nav-list > .active > a > [class*=" icon-"], -.navbar-inverse .nav > .active > a > [class^="icon-"], -.navbar-inverse .nav > .active > a > [class*=" icon-"], -.dropdown-menu > li > a:hover > [class^="icon-"], -.dropdown-menu > li > a:focus > [class^="icon-"], -.dropdown-menu > li > a:hover > [class*=" icon-"], -.dropdown-menu > li > a:focus > [class*=" icon-"], -.dropdown-menu > .active > a > [class^="icon-"], -.dropdown-menu > .active > a > [class*=" icon-"], -.dropdown-submenu:hover > a > [class^="icon-"], -.dropdown-submenu:focus > a > [class^="icon-"], -.dropdown-submenu:hover > a > [class*=" icon-"], -.dropdown-submenu:focus > a > [class*=" icon-"] { - background-image: url("../img/glyphicons-halflings-white.png"); -} - -.icon-glass { - background-position: 0 0; -} - -.icon-music { - background-position: -24px 0; -} - -.icon-search { - background-position: -48px 0; -} - -.icon-envelope { - background-position: -72px 0; -} - -.icon-heart { - background-position: -96px 0; -} - -.icon-star { - background-position: -120px 0; -} - -.icon-star-empty { - background-position: -144px 0; -} - -.icon-user { - background-position: -168px 0; -} - -.icon-film { - background-position: -192px 0; -} - -.icon-th-large { - background-position: -216px 0; -} - -.icon-th { - background-position: -240px 0; -} - -.icon-th-list { - background-position: -264px 0; -} - -.icon-ok { - background-position: -288px 0; -} - -.icon-remove { - background-position: -312px 0; -} - -.icon-zoom-in { - background-position: -336px 0; -} - -.icon-zoom-out { - background-position: -360px 0; -} - -.icon-off { - background-position: -384px 0; -} - -.icon-signal { - background-position: -408px 0; -} - -.icon-cog { - background-position: -432px 0; -} - -.icon-trash { - background-position: -456px 0; -} - -.icon-home { - background-position: 0 -24px; -} - -.icon-file { - background-position: -24px -24px; -} - -.icon-time { - background-position: -48px -24px; -} - -.icon-road { - background-position: -72px -24px; -} - -.icon-download-alt { - background-position: -96px -24px; -} - -.icon-download { - background-position: -120px -24px; -} - -.icon-upload { - background-position: -144px -24px; -} - -.icon-inbox { - background-position: -168px -24px; -} - -.icon-play-circle { - background-position: -192px -24px; -} - -.icon-repeat { - background-position: -216px -24px; -} - -.icon-refresh { - background-position: -240px -24px; -} - -.icon-list-alt { - background-position: -264px -24px; -} - -.icon-lock { - background-position: -287px -24px; -} - -.icon-flag { - background-position: -312px -24px; -} - -.icon-headphones { - background-position: -336px -24px; -} - -.icon-volume-off { - background-position: -360px -24px; -} - -.icon-volume-down { - background-position: -384px -24px; -} - -.icon-volume-up { - background-position: -408px -24px; -} - -.icon-qrcode { - background-position: -432px -24px; -} - -.icon-barcode { - background-position: -456px -24px; -} - -.icon-tag { - background-position: 0 -48px; -} - -.icon-tags { - background-position: -25px -48px; -} - -.icon-book { - background-position: -48px -48px; -} - -.icon-bookmark { - background-position: -72px -48px; -} - -.icon-print { - background-position: -96px -48px; -} - -.icon-camera { - background-position: -120px -48px; -} - -.icon-font { - background-position: -144px -48px; -} - -.icon-bold { - background-position: -167px -48px; -} - -.icon-italic { - background-position: -192px -48px; -} - -.icon-text-height { - background-position: -216px -48px; -} - -.icon-text-width { - background-position: -240px -48px; -} - -.icon-align-left { - background-position: -264px -48px; -} - -.icon-align-center { - background-position: -288px -48px; -} - -.icon-align-right { - background-position: -312px -48px; -} - -.icon-align-justify { - background-position: -336px -48px; -} - -.icon-list { - background-position: -360px -48px; -} - -.icon-indent-left { - background-position: -384px -48px; -} - -.icon-indent-right { - background-position: -408px -48px; -} - -.icon-facetime-video { - background-position: -432px -48px; -} - -.icon-picture { - background-position: -456px -48px; -} - -.icon-pencil { - background-position: 0 -72px; -} - -.icon-map-marker { - background-position: -24px -72px; -} - -.icon-adjust { - background-position: -48px -72px; -} - -.icon-tint { - background-position: -72px -72px; -} - -.icon-edit { - background-position: -96px -72px; -} - -.icon-share { - background-position: -120px -72px; -} - -.icon-check { - background-position: -144px -72px; -} - -.icon-move { - background-position: -168px -72px; -} - -.icon-step-backward { - background-position: -192px -72px; -} - -.icon-fast-backward { - background-position: -216px -72px; -} - -.icon-backward { - background-position: -240px -72px; -} - -.icon-play { - background-position: -264px -72px; -} - -.icon-pause { - background-position: -288px -72px; -} - -.icon-stop { - background-position: -312px -72px; -} - -.icon-forward { - background-position: -336px -72px; -} - -.icon-fast-forward { - background-position: -360px -72px; -} - -.icon-step-forward { - background-position: -384px -72px; -} - -.icon-eject { - background-position: -408px -72px; -} - -.icon-chevron-left { - background-position: -432px -72px; -} - -.icon-chevron-right { - background-position: -456px -72px; -} - -.icon-plus-sign { - background-position: 0 -96px; -} - -.icon-minus-sign { - background-position: -24px -96px; -} - -.icon-remove-sign { - background-position: -48px -96px; -} - -.icon-ok-sign { - background-position: -72px -96px; -} - -.icon-question-sign { - background-position: -96px -96px; -} - -.icon-info-sign { - background-position: -120px -96px; -} - -.icon-screenshot { - background-position: -144px -96px; -} - -.icon-remove-circle { - background-position: -168px -96px; -} - -.icon-ok-circle { - background-position: -192px -96px; -} - -.icon-ban-circle { - background-position: -216px -96px; -} - -.icon-arrow-left { - background-position: -240px -96px; -} - -.icon-arrow-right { - background-position: -264px -96px; -} - -.icon-arrow-up { - background-position: -289px -96px; -} - -.icon-arrow-down { - background-position: -312px -96px; -} - -.icon-share-alt { - background-position: -336px -96px; -} - -.icon-resize-full { - background-position: -360px -96px; -} - -.icon-resize-small { - background-position: -384px -96px; -} - -.icon-plus { - background-position: -408px -96px; -} - -.icon-minus { - background-position: -433px -96px; -} - -.icon-asterisk { - background-position: -456px -96px; -} - -.icon-exclamation-sign { - background-position: 0 -120px; -} - -.icon-gift { - background-position: -24px -120px; -} - -.icon-leaf { - background-position: -48px -120px; -} - -.icon-fire { - background-position: -72px -120px; -} - -.icon-eye-open { - background-position: -96px -120px; -} - -.icon-eye-close { - background-position: -120px -120px; -} - -.icon-warning-sign { - background-position: -144px -120px; -} - -.icon-plane { - background-position: -168px -120px; -} - -.icon-calendar { - background-position: -192px -120px; -} - -.icon-random { - width: 16px; - background-position: -216px -120px; -} - -.icon-comment { - background-position: -240px -120px; -} - -.icon-magnet { - background-position: -264px -120px; -} - -.icon-chevron-up { - background-position: -288px -120px; -} - -.icon-chevron-down { - background-position: -313px -119px; -} - -.icon-retweet { - background-position: -336px -120px; -} - -.icon-shopping-cart { - background-position: -360px -120px; -} - -.icon-folder-close { - width: 16px; - background-position: -384px -120px; -} - -.icon-folder-open { - width: 16px; - background-position: -408px -120px; -} - -.icon-resize-vertical { - background-position: -432px -119px; -} - -.icon-resize-horizontal { - background-position: -456px -118px; -} - -.icon-hdd { - background-position: 0 -144px; -} - -.icon-bullhorn { - background-position: -24px -144px; -} - -.icon-bell { - background-position: -48px -144px; -} - -.icon-certificate { - background-position: -72px -144px; -} - -.icon-thumbs-up { - background-position: -96px -144px; -} - -.icon-thumbs-down { - background-position: -120px -144px; -} - -.icon-hand-right { - background-position: -144px -144px; -} - -.icon-hand-left { - background-position: -168px -144px; -} - -.icon-hand-up { - background-position: -192px -144px; -} - -.icon-hand-down { - background-position: -216px -144px; -} - -.icon-circle-arrow-right { - background-position: -240px -144px; -} - -.icon-circle-arrow-left { - background-position: -264px -144px; -} - -.icon-circle-arrow-up { - background-position: -288px -144px; -} - -.icon-circle-arrow-down { - background-position: -312px -144px; -} - -.icon-globe { - background-position: -336px -144px; -} - -.icon-wrench { - background-position: -360px -144px; -} - -.icon-tasks { - background-position: -384px -144px; -} - -.icon-filter { - background-position: -408px -144px; -} - -.icon-briefcase { - background-position: -432px -144px; -} - -.icon-fullscreen { - background-position: -456px -144px; -} - -.dropup, -.dropdown { - position: relative; -} - -.dropdown-toggle { - *margin-bottom: -3px; -} - -.dropdown-toggle:active, -.open .dropdown-toggle { - outline: 0; -} - -.caret { - display: inline-block; - width: 0; - height: 0; - vertical-align: top; - border-top: 4px solid #000000; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - content: ""; -} - -.dropdown .caret { - margin-top: 8px; - margin-left: 2px; -} - -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - list-style: none; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - *border-right-width: 2px; - *border-bottom-width: 2px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.dropdown-menu .divider { - *width: 100%; - height: 1px; - margin: 9px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: #333333; - white-space: nowrap; -} - -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus, -.dropdown-submenu:hover > a, -.dropdown-submenu:focus > a { - color: #ffffff; - text-decoration: none; - background-color: #0081c2; - background-image: -moz-linear-gradient(top, #0088cc, #0077b3); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); - background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); - background-image: -o-linear-gradient(top, #0088cc, #0077b3); - background-image: linear-gradient(to bottom, #0088cc, #0077b3); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); -} - -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #ffffff; - text-decoration: none; - background-color: #0081c2; - background-image: -moz-linear-gradient(top, #0088cc, #0077b3); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); - background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); - background-image: -o-linear-gradient(top, #0088cc, #0077b3); - background-image: linear-gradient(to bottom, #0088cc, #0077b3); - background-repeat: repeat-x; - outline: 0; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); -} - -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #999999; -} - -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - cursor: default; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.open { - *z-index: 1000; -} - -.open > .dropdown-menu { - display: block; -} - -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} - -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - border-top: 0; - border-bottom: 4px solid #000000; - content: ""; -} - -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 1px; -} - -.dropdown-submenu { - position: relative; -} - -.dropdown-submenu > .dropdown-menu { - top: 0; - left: 100%; - margin-top: -6px; - margin-left: -1px; - -webkit-border-radius: 0 6px 6px 6px; - -moz-border-radius: 0 6px 6px 6px; - border-radius: 0 6px 6px 6px; -} - -.dropdown-submenu:hover > .dropdown-menu { - display: block; -} - -.dropup .dropdown-submenu > .dropdown-menu { - top: auto; - bottom: 0; - margin-top: 0; - margin-bottom: -2px; - -webkit-border-radius: 5px 5px 5px 0; - -moz-border-radius: 5px 5px 5px 0; - border-radius: 5px 5px 5px 0; -} - -.dropdown-submenu > a:after { - display: block; - float: right; - width: 0; - height: 0; - margin-top: 5px; - margin-right: -10px; - border-color: transparent; - border-left-color: #cccccc; - border-style: solid; - border-width: 5px 0 5px 5px; - content: " "; -} - -.dropdown-submenu:hover > a:after { - border-left-color: #ffffff; -} - -.dropdown-submenu.pull-left { - float: none; -} - -.dropdown-submenu.pull-left > .dropdown-menu { - left: -100%; - margin-left: 10px; - -webkit-border-radius: 6px 0 6px 6px; - -moz-border-radius: 6px 0 6px 6px; - border-radius: 6px 0 6px 6px; -} - -.dropdown .dropdown-menu .nav-header { - padding-right: 20px; - padding-left: 20px; -} - -.typeahead { - z-index: 1051; - margin-top: 2px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -} - -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, 0.15); -} - -.well-large { - padding: 24px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.well-small { - padding: 9px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.fade { - opacity: 0; - -webkit-transition: opacity 0.15s linear; - -moz-transition: opacity 0.15s linear; - -o-transition: opacity 0.15s linear; - transition: opacity 0.15s linear; -} - -.fade.in { - opacity: 1; -} - -.collapse { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition: height 0.35s ease; - -moz-transition: height 0.35s ease; - -o-transition: height 0.35s ease; - transition: height 0.35s ease; -} - -.collapse.in { - height: auto; -} - -.close { - float: right; - font-size: 20px; - font-weight: bold; - line-height: 20px; - color: #000000; - text-shadow: 0 1px 0 #ffffff; - opacity: 0.2; - filter: alpha(opacity=20); -} - -.close:hover, -.close:focus { - color: #000000; - text-decoration: none; - cursor: pointer; - opacity: 0.4; - filter: alpha(opacity=40); -} - -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} - -.btn { - display: inline-block; - *display: inline; - padding: 4px 12px; - margin-bottom: 0; - *margin-left: .3em; - font-size: 14px; - line-height: 20px; - color: #333333; - text-align: center; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - vertical-align: middle; - cursor: pointer; - background-color: #f5f5f5; - *background-color: #e6e6e6; - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); - background-repeat: repeat-x; - border: 1px solid #cccccc; - *border: 0; - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - border-bottom-color: #b3b3b3; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn:hover, -.btn:focus, -.btn:active, -.btn.active, -.btn.disabled, -.btn[disabled] { - color: #333333; - background-color: #e6e6e6; - *background-color: #d9d9d9; -} - -.btn:active, -.btn.active { - background-color: #cccccc \9; -} - -.btn:first-child { - *margin-left: 0; -} - -.btn:hover, -.btn:focus { - color: #333333; - text-decoration: none; - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} - -.btn:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -.btn.active, -.btn:active { - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn.disabled, -.btn[disabled] { - cursor: default; - background-image: none; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.btn-large { - padding: 11px 19px; - font-size: 17.5px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.btn-large [class^="icon-"], -.btn-large [class*=" icon-"] { - margin-top: 4px; -} - -.btn-small { - padding: 2px 10px; - font-size: 11.9px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.btn-small [class^="icon-"], -.btn-small [class*=" icon-"] { - margin-top: 0; -} - -.btn-mini [class^="icon-"], -.btn-mini [class*=" icon-"] { - margin-top: -1px; -} - -.btn-mini { - padding: 0 6px; - font-size: 10.5px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.btn-block { - display: block; - width: 100%; - padding-right: 0; - padding-left: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.btn-block + .btn-block { - margin-top: 5px; -} - -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; -} - -.btn-primary.active, -.btn-warning.active, -.btn-danger.active, -.btn-success.active, -.btn-info.active, -.btn-inverse.active { - color: rgba(255, 255, 255, 0.75); -} - -.btn-primary { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #006dcc; - *background-color: #0044cc; - background-image: -moz-linear-gradient(top, #0088cc, #0044cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); - background-image: -o-linear-gradient(top, #0088cc, #0044cc); - background-image: linear-gradient(to bottom, #0088cc, #0044cc); - background-repeat: repeat-x; - border-color: #0044cc #0044cc #002a80; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-primary:hover, -.btn-primary:focus, -.btn-primary:active, -.btn-primary.active, -.btn-primary.disabled, -.btn-primary[disabled] { - color: #ffffff; - background-color: #0044cc; - *background-color: #003bb3; -} - -.btn-primary:active, -.btn-primary.active { - background-color: #003399 \9; -} - -.btn-warning { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #faa732; - *background-color: #f89406; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(to bottom, #fbb450, #f89406); - background-repeat: repeat-x; - border-color: #f89406 #f89406 #ad6704; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-warning:hover, -.btn-warning:focus, -.btn-warning:active, -.btn-warning.active, -.btn-warning.disabled, -.btn-warning[disabled] { - color: #ffffff; - background-color: #f89406; - *background-color: #df8505; -} - -.btn-warning:active, -.btn-warning.active { - background-color: #c67605 \9; -} - -.btn-danger { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #da4f49; - *background-color: #bd362f; - background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); - background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); - background-repeat: repeat-x; - border-color: #bd362f #bd362f #802420; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-danger:hover, -.btn-danger:focus, -.btn-danger:active, -.btn-danger.active, -.btn-danger.disabled, -.btn-danger[disabled] { - color: #ffffff; - background-color: #bd362f; - *background-color: #a9302a; -} - -.btn-danger:active, -.btn-danger.active { - background-color: #942a25 \9; -} - -.btn-success { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #5bb75b; - *background-color: #51a351; - background-image: -moz-linear-gradient(top, #62c462, #51a351); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); - background-image: -webkit-linear-gradient(top, #62c462, #51a351); - background-image: -o-linear-gradient(top, #62c462, #51a351); - background-image: linear-gradient(to bottom, #62c462, #51a351); - background-repeat: repeat-x; - border-color: #51a351 #51a351 #387038; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-success:hover, -.btn-success:focus, -.btn-success:active, -.btn-success.active, -.btn-success.disabled, -.btn-success[disabled] { - color: #ffffff; - background-color: #51a351; - *background-color: #499249; -} - -.btn-success:active, -.btn-success.active { - background-color: #408140 \9; -} - -.btn-info { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #49afcd; - *background-color: #2f96b4; - background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); - background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); - background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); - background-repeat: repeat-x; - border-color: #2f96b4 #2f96b4 #1f6377; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-info:hover, -.btn-info:focus, -.btn-info:active, -.btn-info.active, -.btn-info.disabled, -.btn-info[disabled] { - color: #ffffff; - background-color: #2f96b4; - *background-color: #2a85a0; -} - -.btn-info:active, -.btn-info.active { - background-color: #24748c \9; -} - -.btn-inverse { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #363636; - *background-color: #222222; - background-image: -moz-linear-gradient(top, #444444, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); - background-image: -webkit-linear-gradient(top, #444444, #222222); - background-image: -o-linear-gradient(top, #444444, #222222); - background-image: linear-gradient(to bottom, #444444, #222222); - background-repeat: repeat-x; - border-color: #222222 #222222 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-inverse:hover, -.btn-inverse:focus, -.btn-inverse:active, -.btn-inverse.active, -.btn-inverse.disabled, -.btn-inverse[disabled] { - color: #ffffff; - background-color: #222222; - *background-color: #151515; -} - -.btn-inverse:active, -.btn-inverse.active { - background-color: #080808 \9; -} - -button.btn, -input[type="submit"].btn { - *padding-top: 3px; - *padding-bottom: 3px; -} - -button.btn::-moz-focus-inner, -input[type="submit"].btn::-moz-focus-inner { - padding: 0; - border: 0; -} - -button.btn.btn-large, -input[type="submit"].btn.btn-large { - *padding-top: 7px; - *padding-bottom: 7px; -} - -button.btn.btn-small, -input[type="submit"].btn.btn-small { - *padding-top: 3px; - *padding-bottom: 3px; -} - -button.btn.btn-mini, -input[type="submit"].btn.btn-mini { - *padding-top: 1px; - *padding-bottom: 1px; -} - -.btn-link, -.btn-link:active, -.btn-link[disabled] { - background-color: transparent; - background-image: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.btn-link { - color: #0088cc; - cursor: pointer; - border-color: transparent; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-link:hover, -.btn-link:focus { - color: #005580; - text-decoration: underline; - background-color: transparent; -} - -.btn-link[disabled]:hover, -.btn-link[disabled]:focus { - color: #333333; - text-decoration: none; -} - -.btn-group { - position: relative; - display: inline-block; - *display: inline; - *margin-left: .3em; - font-size: 0; - white-space: nowrap; - vertical-align: middle; - *zoom: 1; -} - -.btn-group:first-child { - *margin-left: 0; -} - -.btn-group + .btn-group { - margin-left: 5px; -} - -.btn-toolbar { - margin-top: 10px; - margin-bottom: 10px; - font-size: 0; -} - -.btn-toolbar > .btn + .btn, -.btn-toolbar > .btn-group + .btn, -.btn-toolbar > .btn + .btn-group { - margin-left: 5px; -} - -.btn-group > .btn { - position: relative; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-group > .btn + .btn { - margin-left: -1px; -} - -.btn-group > .btn, -.btn-group > .dropdown-menu, -.btn-group > .popover { - font-size: 14px; -} - -.btn-group > .btn-mini { - font-size: 10.5px; -} - -.btn-group > .btn-small { - font-size: 11.9px; -} - -.btn-group > .btn-large { - font-size: 17.5px; -} - -.btn-group > .btn:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-topleft: 4px; -} - -.btn-group > .btn:last-child, -.btn-group > .dropdown-toggle { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-bottomright: 4px; -} - -.btn-group > .btn.large:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 6px; - border-bottom-left-radius: 6px; - -webkit-border-top-left-radius: 6px; - border-top-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-topleft: 6px; -} - -.btn-group > .btn.large:last-child, -.btn-group > .large.dropdown-toggle { - -webkit-border-top-right-radius: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-topright: 6px; - -moz-border-radius-bottomright: 6px; -} - -.btn-group > .btn:hover, -.btn-group > .btn:focus, -.btn-group > .btn:active, -.btn-group > .btn.active { - z-index: 2; -} - -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} - -.btn-group > .btn + .dropdown-toggle { - *padding-top: 5px; - padding-right: 8px; - *padding-bottom: 5px; - padding-left: 8px; - -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group > .btn-mini + .dropdown-toggle { - *padding-top: 2px; - padding-right: 5px; - *padding-bottom: 2px; - padding-left: 5px; -} - -.btn-group > .btn-small + .dropdown-toggle { - *padding-top: 5px; - *padding-bottom: 4px; -} - -.btn-group > .btn-large + .dropdown-toggle { - *padding-top: 7px; - padding-right: 12px; - *padding-bottom: 7px; - padding-left: 12px; -} - -.btn-group.open .dropdown-toggle { - background-image: none; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group.open .btn.dropdown-toggle { - background-color: #e6e6e6; -} - -.btn-group.open .btn-primary.dropdown-toggle { - background-color: #0044cc; -} - -.btn-group.open .btn-warning.dropdown-toggle { - background-color: #f89406; -} - -.btn-group.open .btn-danger.dropdown-toggle { - background-color: #bd362f; -} - -.btn-group.open .btn-success.dropdown-toggle { - background-color: #51a351; -} - -.btn-group.open .btn-info.dropdown-toggle { - background-color: #2f96b4; -} - -.btn-group.open .btn-inverse.dropdown-toggle { - background-color: #222222; -} - -.btn .caret { - margin-top: 8px; - margin-left: 0; -} - -.btn-large .caret { - margin-top: 6px; -} - -.btn-large .caret { - border-top-width: 5px; - border-right-width: 5px; - border-left-width: 5px; -} - -.btn-mini .caret, -.btn-small .caret { - margin-top: 8px; -} - -.dropup .btn-large .caret { - border-bottom-width: 5px; -} - -.btn-primary .caret, -.btn-warning .caret, -.btn-danger .caret, -.btn-info .caret, -.btn-success .caret, -.btn-inverse .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.btn-group-vertical { - display: inline-block; - *display: inline; - /* IE7 inline-block hack */ - - *zoom: 1; -} - -.btn-group-vertical > .btn { - display: block; - float: none; - max-width: 100%; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-group-vertical > .btn + .btn { - margin-top: -1px; - margin-left: 0; -} - -.btn-group-vertical > .btn:first-child { - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.btn-group-vertical > .btn:last-child { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.btn-group-vertical > .btn-large:first-child { - -webkit-border-radius: 6px 6px 0 0; - -moz-border-radius: 6px 6px 0 0; - border-radius: 6px 6px 0 0; -} - -.btn-group-vertical > .btn-large:last-child { - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; -} - -.alert { - padding: 8px 35px 8px 14px; - margin-bottom: 20px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.alert, -.alert h4 { - color: #c09853; -} - -.alert h4 { - margin: 0; -} - -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: 20px; -} - -.alert-success { - color: #468847; - background-color: #dff0d8; - border-color: #d6e9c6; -} - -.alert-success h4 { - color: #468847; -} - -.alert-danger, -.alert-error { - color: #b94a48; - background-color: #f2dede; - border-color: #eed3d7; -} - -.alert-danger h4, -.alert-error h4 { - color: #b94a48; -} - -.alert-info { - color: #3a87ad; - background-color: #d9edf7; - border-color: #bce8f1; -} - -.alert-info h4 { - color: #3a87ad; -} - -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} - -.alert-block > p, -.alert-block > ul { - margin-bottom: 0; -} - -.alert-block p + p { - margin-top: 5px; -} - -.nav { - margin-bottom: 20px; - margin-left: 0; - list-style: none; -} - -.nav > li > a { - display: block; -} - -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #eeeeee; -} - -.nav > li > a > img { - max-width: none; -} - -.nav > .pull-right { - float: right; -} - -.nav-header { - display: block; - padding: 3px 15px; - font-size: 11px; - font-weight: bold; - line-height: 20px; - color: #999999; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-transform: uppercase; -} - -.nav li + .nav-header { - margin-top: 9px; -} - -.nav-list { - padding-right: 15px; - padding-left: 15px; - margin-bottom: 0; -} - -.nav-list > li > a, -.nav-list .nav-header { - margin-right: -15px; - margin-left: -15px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} - -.nav-list > li > a { - padding: 3px 15px; -} - -.nav-list > .active > a, -.nav-list > .active > a:hover, -.nav-list > .active > a:focus { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); - background-color: #0088cc; -} - -.nav-list [class^="icon-"], -.nav-list [class*=" icon-"] { - margin-right: 2px; -} - -.nav-list .divider { - *width: 100%; - height: 1px; - margin: 9px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.nav-tabs, -.nav-pills { - *zoom: 1; -} - -.nav-tabs:before, -.nav-pills:before, -.nav-tabs:after, -.nav-pills:after { - display: table; - line-height: 0; - content: ""; -} - -.nav-tabs:after, -.nav-pills:after { - clear: both; -} - -.nav-tabs > li, -.nav-pills > li { - float: left; -} - -.nav-tabs > li > a, -.nav-pills > li > a { - padding-right: 12px; - padding-left: 12px; - margin-right: 2px; - line-height: 14px; -} - -.nav-tabs { - border-bottom: 1px solid #ddd; -} - -.nav-tabs > li { - margin-bottom: -1px; -} - -.nav-tabs > li > a { - padding-top: 8px; - padding-bottom: 8px; - line-height: 20px; - border: 1px solid transparent; - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.nav-tabs > li > a:hover, -.nav-tabs > li > a:focus { - border-color: #eeeeee #eeeeee #dddddd; -} - -.nav-tabs > .active > a, -.nav-tabs > .active > a:hover, -.nav-tabs > .active > a:focus { - color: #555555; - cursor: default; - background-color: #ffffff; - border: 1px solid #ddd; - border-bottom-color: transparent; -} - -.nav-pills > li > a { - padding-top: 8px; - padding-bottom: 8px; - margin-top: 2px; - margin-bottom: 2px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.nav-pills > .active > a, -.nav-pills > .active > a:hover, -.nav-pills > .active > a:focus { - color: #ffffff; - background-color: #0088cc; -} - -.nav-stacked > li { - float: none; -} - -.nav-stacked > li > a { - margin-right: 0; -} - -.nav-tabs.nav-stacked { - border-bottom: 0; -} - -.nav-tabs.nav-stacked > li > a { - border: 1px solid #ddd; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.nav-tabs.nav-stacked > li:first-child > a { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-topleft: 4px; -} - -.nav-tabs.nav-stacked > li:last-child > a { - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -moz-border-radius-bottomright: 4px; - -moz-border-radius-bottomleft: 4px; -} - -.nav-tabs.nav-stacked > li > a:hover, -.nav-tabs.nav-stacked > li > a:focus { - z-index: 2; - border-color: #ddd; -} - -.nav-pills.nav-stacked > li > a { - margin-bottom: 3px; -} - -.nav-pills.nav-stacked > li:last-child > a { - margin-bottom: 1px; -} - -.nav-tabs .dropdown-menu { - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; -} - -.nav-pills .dropdown-menu { - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.nav .dropdown-toggle .caret { - margin-top: 6px; - border-top-color: #0088cc; - border-bottom-color: #0088cc; -} - -.nav .dropdown-toggle:hover .caret, -.nav .dropdown-toggle:focus .caret { - border-top-color: #005580; - border-bottom-color: #005580; -} - -/* move down carets for tabs */ - -.nav-tabs .dropdown-toggle .caret { - margin-top: 8px; -} - -.nav .active .dropdown-toggle .caret { - border-top-color: #fff; - border-bottom-color: #fff; -} - -.nav-tabs .active .dropdown-toggle .caret { - border-top-color: #555555; - border-bottom-color: #555555; -} - -.nav > .dropdown.active > a:hover, -.nav > .dropdown.active > a:focus { - cursor: pointer; -} - -.nav-tabs .open .dropdown-toggle, -.nav-pills .open .dropdown-toggle, -.nav > li.dropdown.open.active > a:hover, -.nav > li.dropdown.open.active > a:focus { - color: #ffffff; - background-color: #999999; - border-color: #999999; -} - -.nav li.dropdown.open .caret, -.nav li.dropdown.open.active .caret, -.nav li.dropdown.open a:hover .caret, -.nav li.dropdown.open a:focus .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; - opacity: 1; - filter: alpha(opacity=100); -} - -.tabs-stacked .open > a:hover, -.tabs-stacked .open > a:focus { - border-color: #999999; -} - -.tabbable { - *zoom: 1; -} - -.tabbable:before, -.tabbable:after { - display: table; - line-height: 0; - content: ""; -} - -.tabbable:after { - clear: both; -} - -.tab-content { - overflow: auto; -} - -.tabs-below > .nav-tabs, -.tabs-right > .nav-tabs, -.tabs-left > .nav-tabs { - border-bottom: 0; -} - -.tab-content > .tab-pane, -.pill-content > .pill-pane { - display: none; -} - -.tab-content > .active, -.pill-content > .active { - display: block; -} - -.tabs-below > .nav-tabs { - border-top: 1px solid #ddd; -} - -.tabs-below > .nav-tabs > li { - margin-top: -1px; - margin-bottom: 0; -} - -.tabs-below > .nav-tabs > li > a { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.tabs-below > .nav-tabs > li > a:hover, -.tabs-below > .nav-tabs > li > a:focus { - border-top-color: #ddd; - border-bottom-color: transparent; -} - -.tabs-below > .nav-tabs > .active > a, -.tabs-below > .nav-tabs > .active > a:hover, -.tabs-below > .nav-tabs > .active > a:focus { - border-color: transparent #ddd #ddd #ddd; -} - -.tabs-left > .nav-tabs > li, -.tabs-right > .nav-tabs > li { - float: none; -} - -.tabs-left > .nav-tabs > li > a, -.tabs-right > .nav-tabs > li > a { - min-width: 74px; - margin-right: 0; - margin-bottom: 3px; -} - -.tabs-left > .nav-tabs { - float: left; - margin-right: 19px; - border-right: 1px solid #ddd; -} - -.tabs-left > .nav-tabs > li > a { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.tabs-left > .nav-tabs > li > a:hover, -.tabs-left > .nav-tabs > li > a:focus { - border-color: #eeeeee #dddddd #eeeeee #eeeeee; -} - -.tabs-left > .nav-tabs .active > a, -.tabs-left > .nav-tabs .active > a:hover, -.tabs-left > .nav-tabs .active > a:focus { - border-color: #ddd transparent #ddd #ddd; - *border-right-color: #ffffff; -} - -.tabs-right > .nav-tabs { - float: right; - margin-left: 19px; - border-left: 1px solid #ddd; -} - -.tabs-right > .nav-tabs > li > a { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.tabs-right > .nav-tabs > li > a:hover, -.tabs-right > .nav-tabs > li > a:focus { - border-color: #eeeeee #eeeeee #eeeeee #dddddd; -} - -.tabs-right > .nav-tabs .active > a, -.tabs-right > .nav-tabs .active > a:hover, -.tabs-right > .nav-tabs .active > a:focus { - border-color: #ddd #ddd #ddd transparent; - *border-left-color: #ffffff; -} - -.nav > .disabled > a { - color: #999999; -} - -.nav > .disabled > a:hover, -.nav > .disabled > a:focus { - text-decoration: none; - cursor: default; - background-color: transparent; -} - -.navbar { - *position: relative; - *z-index: 2; - margin-bottom: 20px; - overflow: visible; -} - -.navbar-inner { - min-height: 40px; - padding-right: 20px; - padding-left: 20px; - background-color: #fafafa; - background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); - background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); - background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); - background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); - background-repeat: repeat-x; - border: 1px solid #d4d4d4; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); - *zoom: 1; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); -} - -.navbar-inner:before, -.navbar-inner:after { - display: table; - line-height: 0; - content: ""; -} - -.navbar-inner:after { - clear: both; -} - -.navbar .container { - width: auto; -} - -.nav-collapse.collapse { - height: auto; - overflow: visible; -} - -.navbar .brand { - display: block; - float: left; - padding: 10px 20px 10px; - margin-left: -20px; - font-size: 20px; - font-weight: 200; - color: #777777; - text-shadow: 0 1px 0 #ffffff; -} - -.navbar .brand:hover, -.navbar .brand:focus { - text-decoration: none; -} - -.navbar-text { - margin-bottom: 0; - line-height: 40px; - color: #777777; -} - -.navbar-link { - color: #777777; -} - -.navbar-link:hover, -.navbar-link:focus { - color: #333333; -} - -.navbar .divider-vertical { - height: 40px; - margin: 0 9px; - border-right: 1px solid #ffffff; - border-left: 1px solid #f2f2f2; -} - -.navbar .btn, -.navbar .btn-group { - margin-top: 5px; -} - -.navbar .btn-group .btn, -.navbar .input-prepend .btn, -.navbar .input-append .btn, -.navbar .input-prepend .btn-group, -.navbar .input-append .btn-group { - margin-top: 0; -} - -.navbar-form { - margin-bottom: 0; - *zoom: 1; -} - -.navbar-form:before, -.navbar-form:after { - display: table; - line-height: 0; - content: ""; -} - -.navbar-form:after { - clear: both; -} - -.navbar-form input, -.navbar-form select, -.navbar-form .radio, -.navbar-form .checkbox { - margin-top: 5px; -} - -.navbar-form input, -.navbar-form select, -.navbar-form .btn { - display: inline-block; - margin-bottom: 0; -} - -.navbar-form input[type="image"], -.navbar-form input[type="checkbox"], -.navbar-form input[type="radio"] { - margin-top: 3px; -} - -.navbar-form .input-append, -.navbar-form .input-prepend { - margin-top: 5px; - white-space: nowrap; -} - -.navbar-form .input-append input, -.navbar-form .input-prepend input { - margin-top: 0; -} - -.navbar-search { - position: relative; - float: left; - margin-top: 5px; - margin-bottom: 0; -} - -.navbar-search .search-query { - padding: 4px 14px; - margin-bottom: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - font-weight: normal; - line-height: 1; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -.navbar-static-top { - position: static; - margin-bottom: 0; -} - -.navbar-static-top .navbar-inner { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; - margin-bottom: 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-static-top .navbar-inner { - border-width: 0 0 1px; -} - -.navbar-fixed-bottom .navbar-inner { - border-width: 1px 0 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-fixed-bottom .navbar-inner { - padding-right: 0; - padding-left: 0; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.navbar-static-top .container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.navbar-fixed-top { - top: 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-static-top .navbar-inner { - -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); -} - -.navbar-fixed-bottom { - bottom: 0; -} - -.navbar-fixed-bottom .navbar-inner { - -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); -} - -.navbar .nav { - position: relative; - left: 0; - display: block; - float: left; - margin: 0 10px 0 0; -} - -.navbar .nav.pull-right { - float: right; - margin-right: 0; -} - -.navbar .nav > li { - float: left; -} - -.navbar .nav > li > a { - float: none; - padding: 10px 15px 10px; - color: #777777; - text-decoration: none; - text-shadow: 0 1px 0 #ffffff; -} - -.navbar .nav .dropdown-toggle .caret { - margin-top: 8px; -} - -.navbar .nav > li > a:focus, -.navbar .nav > li > a:hover { - color: #333333; - text-decoration: none; - background-color: transparent; -} - -.navbar .nav > .active > a, -.navbar .nav > .active > a:hover, -.navbar .nav > .active > a:focus { - color: #555555; - text-decoration: none; - background-color: #e5e5e5; - -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); -} - -.navbar .btn-navbar { - display: none; - float: right; - padding: 7px 10px; - margin-right: 5px; - margin-left: 5px; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #ededed; - *background-color: #e5e5e5; - background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); - background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); - background-repeat: repeat-x; - border-color: #e5e5e5 #e5e5e5 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); -} - -.navbar .btn-navbar:hover, -.navbar .btn-navbar:focus, -.navbar .btn-navbar:active, -.navbar .btn-navbar.active, -.navbar .btn-navbar.disabled, -.navbar .btn-navbar[disabled] { - color: #ffffff; - background-color: #e5e5e5; - *background-color: #d9d9d9; -} - -.navbar .btn-navbar:active, -.navbar .btn-navbar.active { - background-color: #cccccc \9; -} - -.navbar .btn-navbar .icon-bar { - display: block; - width: 18px; - height: 2px; - background-color: #f5f5f5; - -webkit-border-radius: 1px; - -moz-border-radius: 1px; - border-radius: 1px; - -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); -} - -.btn-navbar .icon-bar + .icon-bar { - margin-top: 3px; -} - -.navbar .nav > li > .dropdown-menu:before { - position: absolute; - top: -7px; - left: 9px; - display: inline-block; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-left: 7px solid transparent; - border-bottom-color: rgba(0, 0, 0, 0.2); - content: ''; -} - -.navbar .nav > li > .dropdown-menu:after { - position: absolute; - top: -6px; - left: 10px; - display: inline-block; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - border-left: 6px solid transparent; - content: ''; -} - -.navbar-fixed-bottom .nav > li > .dropdown-menu:before { - top: auto; - bottom: -7px; - border-top: 7px solid #ccc; - border-bottom: 0; - border-top-color: rgba(0, 0, 0, 0.2); -} - -.navbar-fixed-bottom .nav > li > .dropdown-menu:after { - top: auto; - bottom: -6px; - border-top: 6px solid #ffffff; - border-bottom: 0; -} - -.navbar .nav li.dropdown > a:hover .caret, -.navbar .nav li.dropdown > a:focus .caret { - border-top-color: #333333; - border-bottom-color: #333333; -} - -.navbar .nav li.dropdown.open > .dropdown-toggle, -.navbar .nav li.dropdown.active > .dropdown-toggle, -.navbar .nav li.dropdown.open.active > .dropdown-toggle { - color: #555555; - background-color: #e5e5e5; -} - -.navbar .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: #777777; - border-bottom-color: #777777; -} - -.navbar .nav li.dropdown.open > .dropdown-toggle .caret, -.navbar .nav li.dropdown.active > .dropdown-toggle .caret, -.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: #555555; - border-bottom-color: #555555; -} - -.navbar .pull-right > li > .dropdown-menu, -.navbar .nav > li > .dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu:before, -.navbar .nav > li > .dropdown-menu.pull-right:before { - right: 12px; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu:after, -.navbar .nav > li > .dropdown-menu.pull-right:after { - right: 13px; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu .dropdown-menu, -.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { - right: 100%; - left: auto; - margin-right: -1px; - margin-left: 0; - -webkit-border-radius: 6px 0 6px 6px; - -moz-border-radius: 6px 0 6px 6px; - border-radius: 6px 0 6px 6px; -} - -.navbar-inverse .navbar-inner { - background-color: #1b1b1b; - background-image: -moz-linear-gradient(top, #222222, #111111); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); - background-image: -webkit-linear-gradient(top, #222222, #111111); - background-image: -o-linear-gradient(top, #222222, #111111); - background-image: linear-gradient(to bottom, #222222, #111111); - background-repeat: repeat-x; - border-color: #252525; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); -} - -.navbar-inverse .brand, -.navbar-inverse .nav > li > a { - color: #999999; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} - -.navbar-inverse .brand:hover, -.navbar-inverse .nav > li > a:hover, -.navbar-inverse .brand:focus, -.navbar-inverse .nav > li > a:focus { - color: #ffffff; -} - -.navbar-inverse .brand { - color: #999999; -} - -.navbar-inverse .navbar-text { - color: #999999; -} - -.navbar-inverse .nav > li > a:focus, -.navbar-inverse .nav > li > a:hover { - color: #ffffff; - background-color: transparent; -} - -.navbar-inverse .nav .active > a, -.navbar-inverse .nav .active > a:hover, -.navbar-inverse .nav .active > a:focus { - color: #ffffff; - background-color: #111111; -} - -.navbar-inverse .navbar-link { - color: #999999; -} - -.navbar-inverse .navbar-link:hover, -.navbar-inverse .navbar-link:focus { - color: #ffffff; -} - -.navbar-inverse .divider-vertical { - border-right-color: #222222; - border-left-color: #111111; -} - -.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, -.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, -.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { - color: #ffffff; - background-color: #111111; -} - -.navbar-inverse .nav li.dropdown > a:hover .caret, -.navbar-inverse .nav li.dropdown > a:focus .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: #999999; - border-bottom-color: #999999; -} - -.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, -.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, -.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.navbar-inverse .navbar-search .search-query { - color: #ffffff; - background-color: #515151; - border-color: #111111; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -webkit-transition: none; - -moz-transition: none; - -o-transition: none; - transition: none; -} - -.navbar-inverse .navbar-search .search-query:-moz-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query:focus, -.navbar-inverse .navbar-search .search-query.focused { - padding: 5px 15px; - color: #333333; - text-shadow: 0 1px 0 #ffffff; - background-color: #ffffff; - border: 0; - outline: 0; - -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); -} - -.navbar-inverse .btn-navbar { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e0e0e; - *background-color: #040404; - background-image: -moz-linear-gradient(top, #151515, #040404); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); - background-image: -webkit-linear-gradient(top, #151515, #040404); - background-image: -o-linear-gradient(top, #151515, #040404); - background-image: linear-gradient(to bottom, #151515, #040404); - background-repeat: repeat-x; - border-color: #040404 #040404 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.navbar-inverse .btn-navbar:hover, -.navbar-inverse .btn-navbar:focus, -.navbar-inverse .btn-navbar:active, -.navbar-inverse .btn-navbar.active, -.navbar-inverse .btn-navbar.disabled, -.navbar-inverse .btn-navbar[disabled] { - color: #ffffff; - background-color: #040404; - *background-color: #000000; -} - -.navbar-inverse .btn-navbar:active, -.navbar-inverse .btn-navbar.active { - background-color: #000000 \9; -} - -.breadcrumb { - padding: 8px 15px; - margin: 0 0 20px; - list-style: none; - background-color: #f5f5f5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.breadcrumb > li { - display: inline-block; - *display: inline; - text-shadow: 0 1px 0 #ffffff; - *zoom: 1; -} - -.breadcrumb > li > .divider { - padding: 0 5px; - color: #ccc; -} - -.breadcrumb > .active { - color: #999999; -} - -.pagination { - margin: 20px 0; -} - -.pagination ul { - display: inline-block; - *display: inline; - margin-bottom: 0; - margin-left: 0; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - *zoom: 1; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.pagination ul > li { - display: inline; -} - -.pagination ul > li > a, -.pagination ul > li > span { - float: left; - padding: 4px 12px; - line-height: 20px; - text-decoration: none; - background-color: #ffffff; - border: 1px solid #dddddd; - border-left-width: 0; -} - -.pagination ul > li > a:hover, -.pagination ul > li > a:focus, -.pagination ul > .active > a, -.pagination ul > .active > span { - background-color: #f5f5f5; -} - -.pagination ul > .active > a, -.pagination ul > .active > span { - color: #999999; - cursor: default; -} - -.pagination ul > .disabled > span, -.pagination ul > .disabled > a, -.pagination ul > .disabled > a:hover, -.pagination ul > .disabled > a:focus { - color: #999999; - cursor: default; - background-color: transparent; -} - -.pagination ul > li:first-child > a, -.pagination ul > li:first-child > span { - border-left-width: 1px; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-topleft: 4px; -} - -.pagination ul > li:last-child > a, -.pagination ul > li:last-child > span { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-bottomright: 4px; -} - -.pagination-centered { - text-align: center; -} - -.pagination-right { - text-align: right; -} - -.pagination-large ul > li > a, -.pagination-large ul > li > span { - padding: 11px 19px; - font-size: 17.5px; -} - -.pagination-large ul > li:first-child > a, -.pagination-large ul > li:first-child > span { - -webkit-border-bottom-left-radius: 6px; - border-bottom-left-radius: 6px; - -webkit-border-top-left-radius: 6px; - border-top-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-topleft: 6px; -} - -.pagination-large ul > li:last-child > a, -.pagination-large ul > li:last-child > span { - -webkit-border-top-right-radius: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-topright: 6px; - -moz-border-radius-bottomright: 6px; -} - -.pagination-mini ul > li:first-child > a, -.pagination-small ul > li:first-child > a, -.pagination-mini ul > li:first-child > span, -.pagination-small ul > li:first-child > span { - -webkit-border-bottom-left-radius: 3px; - border-bottom-left-radius: 3px; - -webkit-border-top-left-radius: 3px; - border-top-left-radius: 3px; - -moz-border-radius-bottomleft: 3px; - -moz-border-radius-topleft: 3px; -} - -.pagination-mini ul > li:last-child > a, -.pagination-small ul > li:last-child > a, -.pagination-mini ul > li:last-child > span, -.pagination-small ul > li:last-child > span { - -webkit-border-top-right-radius: 3px; - border-top-right-radius: 3px; - -webkit-border-bottom-right-radius: 3px; - border-bottom-right-radius: 3px; - -moz-border-radius-topright: 3px; - -moz-border-radius-bottomright: 3px; -} - -.pagination-small ul > li > a, -.pagination-small ul > li > span { - padding: 2px 10px; - font-size: 11.9px; -} - -.pagination-mini ul > li > a, -.pagination-mini ul > li > span { - padding: 0 6px; - font-size: 10.5px; -} - -.pager { - margin: 20px 0; - text-align: center; - list-style: none; - *zoom: 1; -} - -.pager:before, -.pager:after { - display: table; - line-height: 0; - content: ""; -} - -.pager:after { - clear: both; -} - -.pager li { - display: inline; -} - -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #f5f5f5; -} - -.pager .next > a, -.pager .next > span { - float: right; -} - -.pager .previous > a, -.pager .previous > span { - float: left; -} - -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #999999; - cursor: default; - background-color: #fff; -} - -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000000; -} - -.modal-backdrop.fade { - opacity: 0; -} - -.modal-backdrop, -.modal-backdrop.fade.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.modal { - position: fixed; - top: 10%; - left: 50%; - z-index: 1050; - width: 560px; - margin-left: -280px; - background-color: #ffffff; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, 0.3); - *border: 1px solid #999; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - outline: none; - -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} - -.modal.fade { - top: -25%; - -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; - -moz-transition: opacity 0.3s linear, top 0.3s ease-out; - -o-transition: opacity 0.3s linear, top 0.3s ease-out; - transition: opacity 0.3s linear, top 0.3s ease-out; -} - -.modal.fade.in { - top: 10%; -} - -.modal-header { - padding: 9px 15px; - border-bottom: 1px solid #eee; -} - -.modal-header .close { - margin-top: 2px; -} - -.modal-header h3 { - margin: 0; - line-height: 30px; -} - -.modal-body { - position: relative; - max-height: 400px; - padding: 15px; - overflow-y: auto; -} - -.modal-form { - margin-bottom: 0; -} - -.modal-footer { - padding: 14px 15px 15px; - margin-bottom: 0; - text-align: right; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 #ffffff; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 #ffffff; -} - -.modal-footer:before, -.modal-footer:after { - display: table; - line-height: 0; - content: ""; -} - -.modal-footer:after { - clear: both; -} - -.modal-footer .btn + .btn { - margin-bottom: 0; - margin-left: 5px; -} - -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} - -.modal-footer .btn-block + .btn-block { - margin-left: 0; -} - -.tooltip { - position: absolute; - z-index: 1030; - display: block; - font-size: 11px; - line-height: 1.4; - opacity: 0; - filter: alpha(opacity=0); - visibility: visible; -} - -.tooltip.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.tooltip.top { - padding: 5px 0; - margin-top: -3px; -} - -.tooltip.right { - padding: 0 5px; - margin-left: 3px; -} - -.tooltip.bottom { - padding: 5px 0; - margin-top: 3px; -} - -.tooltip.left { - padding: 0 5px; - margin-left: -3px; -} - -.tooltip-inner { - max-width: 200px; - padding: 8px; - color: #ffffff; - text-align: center; - text-decoration: none; - background-color: #000000; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} - -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-top-color: #000000; - border-width: 5px 5px 0; -} - -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-right-color: #000000; - border-width: 5px 5px 5px 0; -} - -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-left-color: #000000; - border-width: 5px 0 5px 5px; -} - -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-bottom-color: #000000; - border-width: 0 5px 5px; -} - -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1010; - display: none; - max-width: 276px; - padding: 1px; - text-align: left; - white-space: normal; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.popover.top { - margin-top: -10px; -} - -.popover.right { - margin-left: 10px; -} - -.popover.bottom { - margin-top: 10px; -} - -.popover.left { - margin-left: -10px; -} - -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - font-weight: normal; - line-height: 18px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - -webkit-border-radius: 5px 5px 0 0; - -moz-border-radius: 5px 5px 0 0; - border-radius: 5px 5px 0 0; -} - -.popover-title:empty { - display: none; -} - -.popover-content { - padding: 9px 14px; -} - -.popover .arrow, -.popover .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} - -.popover .arrow { - border-width: 11px; -} - -.popover .arrow:after { - border-width: 10px; - content: ""; -} - -.popover.top .arrow { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, 0.25); - border-bottom-width: 0; -} - -.popover.top .arrow:after { - bottom: 1px; - margin-left: -10px; - border-top-color: #ffffff; - border-bottom-width: 0; -} - -.popover.right .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-right-color: #999; - border-right-color: rgba(0, 0, 0, 0.25); - border-left-width: 0; -} - -.popover.right .arrow:after { - bottom: -10px; - left: 1px; - border-right-color: #ffffff; - border-left-width: 0; -} - -.popover.bottom .arrow { - top: -11px; - left: 50%; - margin-left: -11px; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, 0.25); - border-top-width: 0; -} - -.popover.bottom .arrow:after { - top: 1px; - margin-left: -10px; - border-bottom-color: #ffffff; - border-top-width: 0; -} - -.popover.left .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-left-color: #999; - border-left-color: rgba(0, 0, 0, 0.25); - border-right-width: 0; -} - -.popover.left .arrow:after { - right: 1px; - bottom: -10px; - border-left-color: #ffffff; - border-right-width: 0; -} - -.thumbnails { - margin-left: -20px; - list-style: none; - *zoom: 1; -} - -.thumbnails:before, -.thumbnails:after { - display: table; - line-height: 0; - content: ""; -} - -.thumbnails:after { - clear: both; -} - -.row-fluid .thumbnails { - margin-left: 0; -} - -.thumbnails > li { - float: left; - margin-bottom: 20px; - margin-left: 20px; -} - -.thumbnail { - display: block; - padding: 4px; - line-height: 20px; - border: 1px solid #ddd; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; -} - -a.thumbnail:hover, -a.thumbnail:focus { - border-color: #0088cc; - -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -} - -.thumbnail > img { - display: block; - max-width: 100%; - margin-right: auto; - margin-left: auto; -} - -.thumbnail .caption { - padding: 9px; - color: #555555; -} - -.media, -.media-body { - overflow: hidden; - *overflow: visible; - zoom: 1; -} - -.media, -.media .media { - margin-top: 15px; -} - -.media:first-child { - margin-top: 0; -} - -.media-object { - display: block; -} - -.media-heading { - margin: 0 0 5px; -} - -.media > .pull-left { - margin-right: 10px; -} - -.media > .pull-right { - margin-left: 10px; -} - -.media-list { - margin-left: 0; - list-style: none; -} - -.label, -.badge { - display: inline-block; - padding: 2px 4px; - font-size: 11.844px; - font-weight: bold; - line-height: 14px; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - white-space: nowrap; - vertical-align: baseline; - background-color: #999999; -} - -.label { - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.badge { - padding-right: 9px; - padding-left: 9px; - -webkit-border-radius: 9px; - -moz-border-radius: 9px; - border-radius: 9px; -} - -.label:empty, -.badge:empty { - display: none; -} - -a.label:hover, -a.label:focus, -a.badge:hover, -a.badge:focus { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} - -.label-important, -.badge-important { - background-color: #b94a48; -} - -.label-important[href], -.badge-important[href] { - background-color: #953b39; -} - -.label-warning, -.badge-warning { - background-color: #f89406; -} - -.label-warning[href], -.badge-warning[href] { - background-color: #c67605; -} - -.label-success, -.badge-success { - background-color: #468847; -} - -.label-success[href], -.badge-success[href] { - background-color: #356635; -} - -.label-info, -.badge-info { - background-color: #3a87ad; -} - -.label-info[href], -.badge-info[href] { - background-color: #2d6987; -} - -.label-inverse, -.badge-inverse { - background-color: #333333; -} - -.label-inverse[href], -.badge-inverse[href] { - background-color: #1a1a1a; -} - -.btn .label, -.btn .badge { - position: relative; - top: -1px; -} - -.btn-mini .label, -.btn-mini .badge { - top: 0; -} - -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-moz-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-ms-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-o-keyframes progress-bar-stripes { - from { - background-position: 0 0; - } - to { - background-position: 40px 0; - } -} - -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -.progress { - height: 20px; - margin-bottom: 20px; - overflow: hidden; - background-color: #f7f7f7; - background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); - background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); - background-repeat: repeat-x; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.progress .bar { - float: left; - width: 0; - height: 100%; - font-size: 12px; - color: #ffffff; - text-align: center; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e90d2; - background-image: -moz-linear-gradient(top, #149bdf, #0480be); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); - background-image: -webkit-linear-gradient(top, #149bdf, #0480be); - background-image: -o-linear-gradient(top, #149bdf, #0480be); - background-image: linear-gradient(to bottom, #149bdf, #0480be); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: width 0.6s ease; - -moz-transition: width 0.6s ease; - -o-transition: width 0.6s ease; - transition: width 0.6s ease; -} - -.progress .bar + .bar { - -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); -} - -.progress-striped .bar { - background-color: #149bdf; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; -} - -.progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} - -.progress-danger .bar, -.progress .bar-danger { - background-color: #dd514c; - background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); - background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); -} - -.progress-danger.progress-striped .bar, -.progress-striped .bar-danger { - background-color: #ee5f5b; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-success .bar, -.progress .bar-success { - background-color: #5eb95e; - background-image: -moz-linear-gradient(top, #62c462, #57a957); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); - background-image: -webkit-linear-gradient(top, #62c462, #57a957); - background-image: -o-linear-gradient(top, #62c462, #57a957); - background-image: linear-gradient(to bottom, #62c462, #57a957); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); -} - -.progress-success.progress-striped .bar, -.progress-striped .bar-success { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-info .bar, -.progress .bar-info { - background-color: #4bb1cf; - background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); - background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); - background-image: -o-linear-gradient(top, #5bc0de, #339bb9); - background-image: linear-gradient(to bottom, #5bc0de, #339bb9); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); -} - -.progress-info.progress-striped .bar, -.progress-striped .bar-info { - background-color: #5bc0de; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-warning .bar, -.progress .bar-warning { - background-color: #faa732; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(to bottom, #fbb450, #f89406); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); -} - -.progress-warning.progress-striped .bar, -.progress-striped .bar-warning { - background-color: #fbb450; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.accordion { - margin-bottom: 20px; -} - -.accordion-group { - margin-bottom: 2px; - border: 1px solid #e5e5e5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.accordion-heading { - border-bottom: 0; -} - -.accordion-heading .accordion-toggle { - display: block; - padding: 8px 15px; -} - -.accordion-toggle { - cursor: pointer; -} - -.accordion-inner { - padding: 9px 15px; - border-top: 1px solid #e5e5e5; -} - -.carousel { - position: relative; - margin-bottom: 20px; - line-height: 1; -} - -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; -} - -.carousel-inner > .item { - position: relative; - display: none; - -webkit-transition: 0.6s ease-in-out left; - -moz-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; -} - -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - line-height: 1; -} - -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; -} - -.carousel-inner > .active { - left: 0; -} - -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; -} - -.carousel-inner > .next { - left: 100%; -} - -.carousel-inner > .prev { - left: -100%; -} - -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; -} - -.carousel-inner > .active.left { - left: -100%; -} - -.carousel-inner > .active.right { - left: 100%; -} - -.carousel-control { - position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: #ffffff; - text-align: center; - background: #222222; - border: 3px solid #ffffff; - -webkit-border-radius: 23px; - -moz-border-radius: 23px; - border-radius: 23px; - opacity: 0.5; - filter: alpha(opacity=50); -} - -.carousel-control.right { - right: 15px; - left: auto; -} - -.carousel-control:hover, -.carousel-control:focus { - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); -} - -.carousel-indicators { - position: absolute; - top: 15px; - right: 15px; - z-index: 5; - margin: 0; - list-style: none; -} - -.carousel-indicators li { - display: block; - float: left; - width: 10px; - height: 10px; - margin-left: 5px; - text-indent: -999px; - background-color: #ccc; - background-color: rgba(255, 255, 255, 0.25); - border-radius: 5px; -} - -.carousel-indicators .active { - background-color: #fff; -} - -.carousel-caption { - position: absolute; - right: 0; - bottom: 0; - left: 0; - padding: 15px; - background: #333333; - background: rgba(0, 0, 0, 0.75); -} - -.carousel-caption h4, -.carousel-caption p { - line-height: 20px; - color: #ffffff; -} - -.carousel-caption h4 { - margin: 0 0 5px; -} - -.carousel-caption p { - margin-bottom: 0; -} - -.hero-unit { - padding: 60px; - margin-bottom: 30px; - font-size: 18px; - font-weight: 200; - line-height: 30px; - color: inherit; - background-color: #eeeeee; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.hero-unit h1 { - margin-bottom: 0; - font-size: 60px; - line-height: 1; - letter-spacing: -1px; - color: inherit; -} - -.hero-unit li { - line-height: 30px; -} - -.pull-right { - float: right; -} - -.pull-left { - float: left; -} - -.hide { - display: none; -} - -.show { - display: block; -} - -.invisible { - visibility: hidden; -} - -.affix { - position: fixed; -} diff --git a/gae/webapp/static/bootstrap/css/bootstrap.min.css b/gae/webapp/static/bootstrap/css/bootstrap.min.css deleted file mode 100644 index c10c7f4..0000000 --- a/gae/webapp/static/bootstrap/css/bootstrap.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/gae/webapp/static/bootstrap/img/glyphicons-halflings-white.png b/gae/webapp/static/bootstrap/img/glyphicons-halflings-white.png deleted file mode 100644 index 3bf6484..0000000 Binary files a/gae/webapp/static/bootstrap/img/glyphicons-halflings-white.png and /dev/null differ diff --git a/gae/webapp/static/bootstrap/img/glyphicons-halflings.png b/gae/webapp/static/bootstrap/img/glyphicons-halflings.png deleted file mode 100644 index a996999..0000000 Binary files a/gae/webapp/static/bootstrap/img/glyphicons-halflings.png and /dev/null differ diff --git a/gae/webapp/static/bootstrap/js/bootstrap.js b/gae/webapp/static/bootstrap/js/bootstrap.js deleted file mode 100644 index c298ee4..0000000 --- a/gae/webapp/static/bootstrap/js/bootstrap.js +++ /dev/null @@ -1,2276 +0,0 @@ -/* =================================================== - * bootstrap-transition.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#transitions - * =================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) - * ======================================================= */ - - $(function () { - - $.support.transition = (function () { - - var transitionEnd = (function () { - - var el = document.createElement('bootstrap') - , transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd' - , 'MozTransition' : 'transitionend' - , 'OTransition' : 'oTransitionEnd otransitionend' - , 'transition' : 'transitionend' - } - , name - - for (name in transEndEventNames){ - if (el.style[name] !== undefined) { - return transEndEventNames[name] - } - } - - }()) - - return transitionEnd && { - end: transitionEnd - } - - })() - - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-alert.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#alerts - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* ALERT CLASS DEFINITION - * ====================== */ - - var dismiss = '[data-dismiss="alert"]' - , Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype.close = function (e) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = $(selector) - - e && e.preventDefault() - - $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) - - $parent.trigger(e = $.Event('close')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - $parent - .trigger('closed') - .remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent.on($.support.transition.end, removeElement) : - removeElement() - } - - - /* ALERT PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.alert - - $.fn.alert = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('alert') - if (!data) $this.data('alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - /* ALERT NO CONFLICT - * ================= */ - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - /* ALERT DATA-API - * ============== */ - - $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) - -}(window.jQuery);/* ============================================================ - * bootstrap-button.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#buttons - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* BUTTON PUBLIC CLASS DEFINITION - * ============================== */ - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.button.defaults, options) - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - , $el = this.$element - , data = $el.data() - , val = $el.is('input') ? 'val' : 'html' - - state = state + 'Text' - data.resetText || $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d) - }, 0) - } - - Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons-radio"]') - - $parent && $parent - .find('.active') - .removeClass('active') - - this.$element.toggleClass('active') - } - - - /* BUTTON PLUGIN DEFINITION - * ======================== */ - - var old = $.fn.button - - $.fn.button = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('button') - , options = typeof option == 'object' && option - if (!data) $this.data('button', (data = new Button(this, options))) - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.defaults = { - loadingText: 'loading...' - } - - $.fn.button.Constructor = Button - - - /* BUTTON NO CONFLICT - * ================== */ - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - /* BUTTON DATA-API - * =============== */ - - $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-carousel.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#carousel - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CAROUSEL CLASS DEFINITION - * ========================= */ - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.prototype = { - - cycle: function (e) { - if (!e) this.paused = false - if (this.interval) clearInterval(this.interval); - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - return this - } - - , getActiveIndex: function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - return this.$items.index(this.$active) - } - - , to: function (pos) { - var activeIndex = this.getActiveIndex() - , that = this - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) { - return this.$element.one('slid', function () { - that.to(pos) - }) - } - - if (activeIndex == pos) { - return this.pause().cycle() - } - - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - , pause: function (e) { - if (!e) this.paused = true - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - clearInterval(this.interval) - this.interval = null - return this - } - - , next: function () { - if (this.sliding) return - return this.slide('next') - } - - , prev: function () { - if (this.sliding) return - return this.slide('prev') - } - - , slide: function (type, next) { - var $active = this.$element.find('.item.active') - , $next = next || $active[type]() - , isCycling = this.interval - , direction = type == 'next' ? 'left' : 'right' - , fallback = type == 'next' ? 'first' : 'last' - , that = this - , e - - this.sliding = true - - isCycling && this.pause() - - $next = $next.length ? $next : this.$element.find('.item')[fallback]() - - e = $.Event('slide', { - relatedTarget: $next[0] - , direction: direction - }) - - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - this.$element.one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid') }, 0) - }) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid') - } - - isCycling && this.cycle() - - return this - } - - } - - - /* CAROUSEL PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.carousel - - $.fn.carousel = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('carousel') - , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) - , action = typeof option == 'string' ? option : options.slide - if (!data) $this.data('carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - $.fn.carousel.defaults = { - interval: 5000 - , pause: 'hover' - } - - $.fn.carousel.Constructor = Carousel - - - /* CAROUSEL NO CONFLICT - * ==================== */ - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - /* CAROUSEL DATA-API - * ================= */ - - $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , options = $.extend({}, $target.data(), $this.data()) - , slideIndex - - $target.carousel(options) - - if (slideIndex = $this.attr('data-slide-to')) { - $target.data('carousel').pause().to(slideIndex).cycle() - } - - e.preventDefault() - }) - -}(window.jQuery);/* ============================================================= - * bootstrap-collapse.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#collapse - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* COLLAPSE PUBLIC CLASS DEFINITION - * ================================ */ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options.parent) { - this.$parent = $(this.options.parent) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension - , scroll - , actives - , hasData - - if (this.transitioning || this.$element.hasClass('in')) return - - dimension = this.dimension() - scroll = $.camelCase(['scroll', dimension].join('-')) - actives = this.$parent && this.$parent.find('> .accordion-group > .in') - - if (actives && actives.length) { - hasData = actives.data('collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', $.Event('show'), 'shown') - $.support.transition && this.$element[dimension](this.$element[0][scroll]) - } - - , hide: function () { - var dimension - if (this.transitioning || !this.$element.hasClass('in')) return - dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', $.Event('hide'), 'hidden') - this.$element[dimension](0) - } - - , reset: function (size) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function (method, startEvent, completeEvent) { - var that = this - , complete = function () { - if (startEvent.type == 'show') that.reset() - that.transitioning = 0 - that.$element.trigger(completeEvent) - } - - this.$element.trigger(startEvent) - - if (startEvent.isDefaultPrevented()) return - - this.transitioning = 1 - - this.$element[method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - - /* COLLAPSE PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.collapse - - $.fn.collapse = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSE NO CONFLICT - * ==================== */ - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - /* COLLAPSE DATA-API - * ================= */ - - $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - $(target).collapse(option) - }) - -}(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#dropdowns - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function (e) { - var $this = $(this) - , $parent - , isActive - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - $parent.toggleClass('open') - } - - $this.focus() - - return false - } - - , keydown: function (e) { - var $this - , $items - , $active - , $parent - , isActive - , index - - if (!/(38|40|27)/.test(e.keyCode)) return - - $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - if (!isActive || (isActive && e.keyCode == 27)) { - if (e.which == 27) $parent.find(toggle).focus() - return $this.click() - } - - $items = $('[role=menu] li:not(.divider):visible a', $parent) - - if (!$items.length) return - - index = $items.index($items.filter(':focus')) - - if (e.keyCode == 38 && index > 0) index-- // up - if (e.keyCode == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items - .eq(index) - .focus() - } - - } - - function clearMenus() { - $(toggle).each(function () { - getParent($(this)).removeClass('open') - }) - } - - function getParent($this) { - var selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = selector && $(selector) - - if (!$parent || !$parent.length) $parent = $this.parent() - - return $parent - } - - - /* DROPDOWN PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.dropdown - - $.fn.dropdown = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('dropdown') - if (!data) $this.data('dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.dropdown.Constructor = Dropdown - - - /* DROPDOWN NO CONFLICT - * ==================== */ - - $.fn.dropdown.noConflict = function () { - $.fn.dropdown = old - return this - } - - - /* APPLY TO STANDARD DROPDOWN ELEMENTS - * =================================== */ - - $(document) - .on('click.dropdown.data-api', clearMenus) - .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.dropdown-menu', function (e) { e.stopPropagation() }) - .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) - .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) - -}(window.jQuery); -/* ========================================================= - * bootstrap-modal.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#modals - * ========================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================= */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* MODAL CLASS DEFINITION - * ====================== */ - - var Modal = function (element, options) { - this.options = options - this.$element = $(element) - .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) - this.options.remote && this.$element.find('.modal-body').load(this.options.remote) - } - - Modal.prototype = { - - constructor: Modal - - , toggle: function () { - return this[!this.isShown ? 'show' : 'hide']() - } - - , show: function () { - var that = this - , e = $.Event('show') - - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - this.isShown = true - - this.escape() - - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(document.body) //don't move modals dom position - } - - that.$element.show() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element - .addClass('in') - .attr('aria-hidden', false) - - that.enforceFocus() - - transition ? - that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : - that.$element.focus().trigger('shown') - - }) - } - - , hide: function (e) { - e && e.preventDefault() - - var that = this - - e = $.Event('hide') - - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - this.escape() - - $(document).off('focusin.modal') - - this.$element - .removeClass('in') - .attr('aria-hidden', true) - - $.support.transition && this.$element.hasClass('fade') ? - this.hideWithTransition() : - this.hideModal() - } - - , enforceFocus: function () { - var that = this - $(document).on('focusin.modal', function (e) { - if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { - that.$element.focus() - } - }) - } - - , escape: function () { - var that = this - if (this.isShown && this.options.keyboard) { - this.$element.on('keyup.dismiss.modal', function ( e ) { - e.which == 27 && that.hide() - }) - } else if (!this.isShown) { - this.$element.off('keyup.dismiss.modal') - } - } - - , hideWithTransition: function () { - var that = this - , timeout = setTimeout(function () { - that.$element.off($.support.transition.end) - that.hideModal() - }, 500) - - this.$element.one($.support.transition.end, function () { - clearTimeout(timeout) - that.hideModal() - }) - } - - , hideModal: function () { - var that = this - this.$element.hide() - this.backdrop(function () { - that.removeBackdrop() - that.$element.trigger('hidden') - }) - } - - , removeBackdrop: function () { - this.$backdrop && this.$backdrop.remove() - this.$backdrop = null - } - - , backdrop: function (callback) { - var that = this - , animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $('