diff --git a/master/buildbot/config.py b/master/buildbot/config.py index fff2872307b..259b45e1b3a 100644 --- a/master/buildbot/config.py +++ b/master/buildbot/config.py @@ -545,10 +545,14 @@ def load_www(self, filename, config_dict, errors): if not self.www['url'].endswith('/'): self.www['url'] += '/' - + if "extra_js" in self.www and not type(self.www['extra_js'])==list: + raise TypeError("BuildmasterConfig['www']['extra_js'] param must be a list of path") public_html = self.www.get('public_html') - if public_html and not os.path.isdir(public_html): - errors.addError("public_html directory '%s' does not exist" % + if not public_html: + public_html = self.www["public_html"] = os.path.join(os.path.dirname(filename), + "public_html") + if not os.path.isdir(public_html): + errors.addError("www needs an existing public_html directory config, instead of %s" % (public_html,)) diff --git a/master/buildbot/data/changes.py b/master/buildbot/data/changes.py index 2d9d1aa8d9e..a87932769ae 100644 --- a/master/buildbot/data/changes.py +++ b/master/buildbot/data/changes.py @@ -15,6 +15,7 @@ from twisted.internet import defer from twisted.python import log +import datetime from buildbot.data import base, exceptions from buildbot.process import metrics from buildbot.process.users import users @@ -77,14 +78,15 @@ def addChange(self, files=None, comments=None, author=None, revision=None, author, src) else: uid = None - + if isinstance(when_timestamp, datetime.datetime): + when_timestamp = datetime2epoch(when_timestamp) change = { 'changeid': None, # not known yet 'author': author, 'files': files, 'comments': comments, 'revision': revision, - 'when_timestamp': datetime2epoch(when_timestamp), + 'when_timestamp': when_timestamp, 'branch': branch, 'category': category, 'revlink': revlink, diff --git a/master/buildbot/data/connector.py b/master/buildbot/data/connector.py index d88a16e6eb0..4547133a084 100644 --- a/master/buildbot/data/connector.py +++ b/master/buildbot/data/connector.py @@ -30,7 +30,7 @@ class RTypes(object): class Root(base.Endpoint): - pathPattern = ('',) + pathPattern = () def get(self, options, kwargs): return defer.succeed(self.master.data.rootLinks) diff --git a/master/buildbot/scripts/create_master.py b/master/buildbot/scripts/create_master.py index ba5a5f6058c..c69889b2057 100644 --- a/master/buildbot/scripts/create_master.py +++ b/master/buildbot/scripts/create_master.py @@ -24,6 +24,7 @@ from buildbot.master import BuildMaster from buildbot import config as config_module from buildbot import monkeypatches +from buildbot.scripts.update_js import updateJS def makeBasedir(config): if os.path.exists(config['basedir']): @@ -142,6 +143,7 @@ def createMaster(config): makeSampleConfig(config) makePublicHtml(config) makeTemplatesDir(config) + yield updateJS(config) yield createDB(config) if not config['quiet']: diff --git a/master/buildbot/scripts/runner.py b/master/buildbot/scripts/runner.py index 1016cf09f0a..ba21d7cea12 100644 --- a/master/buildbot/scripts/runner.py +++ b/master/buildbot/scripts/runner.py @@ -34,6 +34,7 @@ class UpgradeMasterOptions(base.BasedirMixin, base.SubcommandOptions): subcommandFunction = "buildbot.scripts.upgrade_master.upgradeMaster" optFlags = [ ["quiet", "q", "Do not emit the commands being run"], + ["develop", "d", "link to buildbot dir rather than copy, and dont perform javascript optimization (only work on unix)"], ["replace", "r", "Replace any modified files without confirmation."], ] optParameters = [ @@ -76,6 +77,7 @@ class CreateMasterOptions(base.BasedirMixin, base.SubcommandOptions): "Re-use an existing directory (will not overwrite master.cfg file)"], ["relocatable", "r", "Create a relocatable buildbot.tac"], + ["develop", "d", "link to buildbot dir rather than copy, and dont perform javascript optimization (only work on unix)"], ["no-logrotate", "n", "Do not permit buildmaster rotate logs by itself"] ] @@ -129,6 +131,14 @@ def postOptions(self): raise usage.UsageError("log-count parameter needs to be an int "+ " or None") +class UpdateJSOptions(base.BasedirMixin, base.SubcommandOptions): + subcommandFunction = "buildbot.scripts.update_js.updateJSFunc" + optFlags = [ + ["quiet", "q", "Do not emit the commands being run"], + ["develop", "d", "link to buildbot dir rather than copy, and dont perform javascript optimization (only work on unix)"], + ] + def getSynopsis(self): + return "Usage: buildbot update_js []" class StopOptions(base.BasedirMixin, base.SubcommandOptions): subcommandFunction = "buildbot.scripts.stop.stop" @@ -639,6 +649,8 @@ class Options(usage.Options): "Create and populate a directory for a new buildmaster"], ['upgrade-master', None, UpgradeMasterOptions, "Upgrade an existing buildmaster directory for the current version"], + ['updatejs', None, UpdateJSOptions, + "update the js directory from buildbot sources, and minify the js"], ['start', None, StartOptions, "Start a buildmaster"], ['stop', None, StopOptions, diff --git a/master/buildbot/scripts/update_js.py b/master/buildbot/scripts/update_js.py new file mode 100644 index 00000000000..336e605a4f9 --- /dev/null +++ b/master/buildbot/scripts/update_js.py @@ -0,0 +1,197 @@ +# This file is part of Buildbot. Buildbot is free software: you can +# redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright Buildbot Team Members + +import os, shutil +import urllib2 +import zipfile +import platform +from twisted.internet import defer, threads +from buildbot.util import in_reactor + +js_deps = [("http://download.dojotoolkit.org/release-1.8.1/dojo-release-1.8.1-src.zip", + "dojo-release-1.8.1-src/", "."), + ("https://github.com/kriszyp/xstyle/archive/v0.0.5.zip", + "xstyle-0.0.5/","xstyle"), + ("https://github.com/kriszyp/put-selector/archive/v0.3.2.zip", + "put-selector-0.3.2/","put-selector"), + ("https://github.com/SitePen/dgrid/archive/v0.3.3.zip", + "dgrid-0.3.3/","dgrid"), + ("https://github.com/timrwood/moment/archive/1.7.2.zip","moment-1.7.2/min/moment.min.js","moment.js") + ] +def syncStatic(config,www, workdir, olddir): + """Synchronize the static directory""" + if not config['quiet']: + print "copying the javascript UI to base directory %s" % (config['basedir'],) + source_dir = os.path.join(os.path.dirname(__file__), "..","www","static") + extra_js = www.get('extra_js', []) + if os.path.exists(workdir): + shutil.rmtree(workdir) + if os.path.exists(olddir): + shutil.rmtree(olddir) + if not config['develop'] or platform.system() == "Windows": + shutil.copytree(source_dir, workdir) + for d in extra_js: + if os.path.exists(d) and os.path.isdir(d): + shutil.copytree(d, os.path.join(workdir, "js", os.path.basename(d))) + else: + os.mkdir(workdir) + for d in "img css".split(): + os.symlink(os.path.join(source_dir, d), os.path.join(workdir, d)) + os.mkdir(os.path.join(workdir, "js")) + for f in os.listdir(os.path.join(source_dir,"js")): + os.symlink(os.path.join(source_dir, "js", f), os.path.join(workdir, "js",f)) + for d in extra_js: + if os.path.exists(d) and os.path.isdir(d): + os.symlink(d, os.path.join(workdir, "js", os.path.basename(d))) + +def downloadJSDeps(config, workdir): + if not config['quiet']: + print "Downloading JS dependancies %s" % (config['basedir'],) + depsdir = os.path.join(config['basedir'], ".jsdeps_tarballs") + if not os.path.isdir(depsdir): + os.mkdir(depsdir) + for url, archivedir, archivedest in js_deps: + fn = os.path.join(depsdir, os.path.basename(url)) + if not os.path.exists(fn): + f = urllib2.urlopen(url) + o = open(fn, "wb") + total_size = int(f.info().getheader('Content-Length').strip()) + chunk_size = 1024*1024 + bytes_so_far = 0 + while True: + if not config['quiet']: + print "Downloading %s: %d%%" % (url,100*bytes_so_far/total_size) + chunk = f.read(chunk_size) + bytes_so_far += len(chunk) + if not chunk: + break + o.write(chunk) + o.close() + z = zipfile.ZipFile(fn) + for member in z.infolist(): + if not member.filename.startswith(archivedir): + continue + isdir = member.external_attr & 16 + dest = os.path.join(workdir, "js", archivedest) + if member.filename[len(archivedir):]: + dest = os.path.join(dest, member.filename[len(archivedir):]) + if isdir: + if not os.path.exists(dest): + os.makedirs(dest) + else: + if not config['quiet']: + print "extracting %s" % (member.filename) + o = open(dest,"wb") + o.write(z.read(member)) + o.close() +build_js = """ +dependencies = (function(){ + var _packages = "dojox dijit put-selector lib dgrid xstyle moment %(extra_pkg)s".split(" "); + var packages = []; + for (var i = 0;i<_packages.length; i+=1) { + if (_packages[i].length >1) { + packages.push([ _packages[i], "../"+_packages[i]]); + } + } + return { + basePath: "%(basePath)s", + releaseDir: "%(releaseDir)s", + prefixes:packages, + layers: [ + { + name: "dojo.js", + customBase: true, + dependencies: [ + "dojo/_base/declare", + ] + }, + { + name: "../lib/router.js", + dependencies: [ + "lib.router" + ] + } + ] + }; +}()); +console.log(JSON.stringify(dependencies,null, " ")) +""" +def minifyJS(config, www, workdir): + skip_minify = False + if os.system("java -version"): + print "you need a working version of java for dojo build system to work" + skip_minify = True + + if platform.system() != "Windows" and os.system("node -v"): + print "you need a working version of node.js in your path for dojo build system to work" + skip_minify = True + + if config['develop']: + skip_minify = True + + # Todo: need to find out the best way to distribute minified code in buildbot's sdist. + # we'll sort this out once we have more code and requirements for the whole sdist picture + # Perhaps its even only needed for large scale buildbot where we can require installation of + # node and java in the master, and where people dont care about sdists. + if skip_minify: + os.symlink("js", os.path.join(workdir, "js.built")) + return + + # create the build.js config file that dojo build system is needing + o = open(os.path.join(workdir,"js","build.js"), "w") + extra_js = www.get('extra_js', []) + o.write(build_js% dict(extra_pkg=" ".join([os.path.basename(js) for js in extra_js]), + basePath = os.path.join(workdir,"js"), + releaseDir = os.path.join(workdir,"jsrelease"))) + o.close() + os.chdir(os.path.join(workdir,"js")) + + # Those scripts are part of the dojo tarball that we previously downloaded + if platform.system() == "Windows": + os.system("util/buildscripts/build.bat --bin java -p build.js --release") + else: + os.system("sh util/buildscripts/build.sh --bin node -p build.js --release") + os.rename(os.path.join(workdir,"jsrelease", "dojo"),os.path.join(workdir,"js.built")) + shutil.rmtree(os.path.join(workdir,"jsrelease")) + if not config['quiet']: + print "optimizing the javascript UI for better performance in %s" % (config['basedir'],) + pass + +@defer.inlineCallbacks +def updateJS(config, master_cfg=None): + if not master_cfg: + from upgrade_master import loadConfig # avoid recursive import + master_cfg = loadConfig(config) + if not master_cfg: + www = {} + else: + www = master_cfg.www + workdir = os.path.join(config['basedir'], "public_html", "static.new") + olddir = os.path.join(config['basedir'], "public_html", "static.old") + static = os.path.join(config['basedir'], "public_html", "static") + yield threads.deferToThread(lambda :syncStatic(config, www, workdir, olddir)) + yield threads.deferToThread(lambda :downloadJSDeps(config, workdir)) + yield threads.deferToThread(lambda :minifyJS(config, www, workdir)) + if os.path.exists(static): + os.rename(static, olddir) + os.rename(workdir, static) + if not config['quiet']: + print "javascript UI configured in %s" % (config['basedir'],) + + defer.returnValue(0) + +@in_reactor +def updateJSFunc(config): + return updateJS(config) diff --git a/master/buildbot/scripts/upgrade_master.py b/master/buildbot/scripts/upgrade_master.py index ec8aee0d522..df1dd47c352 100644 --- a/master/buildbot/scripts/upgrade_master.py +++ b/master/buildbot/scripts/upgrade_master.py @@ -26,6 +26,7 @@ from buildbot.master import BuildMaster from buildbot.util import in_reactor from buildbot.scripts import base +from buildbot.scripts.update_js import updateJS def checkBasedir(config): if not config['quiet']: @@ -166,6 +167,7 @@ def upgradeMaster(config, _noMonkey=False): return upgradeFiles(config) + yield updateJS(config, master_cfg) yield upgradeDatabase(config, master_cfg) if not config['quiet']: diff --git a/master/buildbot/test/unit/test_www_ui.py b/master/buildbot/test/unit/test_www_ui.py index 27d89272515..a55dde332b0 100644 --- a/master/buildbot/test/unit/test_www_ui.py +++ b/master/buildbot/test/unit/test_www_ui.py @@ -13,17 +13,14 @@ # # Copyright Buildbot Team Members -import os -from buildbot.www import ui, service +from buildbot.www import ui from buildbot.test.util import www from twisted.trial import unittest -from twisted.internet import defer, reactor -from twisted.python import failure class Test(www.WwwTestMixin, unittest.TestCase): def test_render(self): master = self.make_master(url='h:/a/b/') - rsrc = ui.UIResource(master) + rsrc = ui.UIResource(master, extra_routes=[]) d = self.render_resource(rsrc, ['']) @d.addCallback @@ -31,49 +28,10 @@ def check(rv): self.assertIn('base_url:"h:/a/b/"', rv) return d -try: - from buildbot.test.util.txghost import Ghost - has_ghost= Ghost != None -except ImportError: - # if $REQUIRE_GHOST is set, then fail if it's not found - if os.environ.get('REQUIRE_GHOST'): - raise - has_ghost=False - -class TestGhostPy(www.WwwTestMixin, unittest.TestCase): - if not has_ghost: - skip = "Need Ghost.py to run most of www_ui tests" - - @defer.inlineCallbacks - def setUp(self): - # hack to prevent twisted.web.http to setup a 1 sec callback at init - import twisted - #twisted.internet.base.DelayedCall.debug = True - twisted.web.http._logDateTimeUsers = 1 - # lets resolve the tested port unicity later... - port = 8010 - self.url = 'http://localhost:'+str(port)+"/" - self.master = self.make_master(url=self.url, port=port) - self.svc = service.WWWService(self.master) - yield self.svc.startService() - yield self.svc.reconfigService(self.master.config) - self.ghost = Ghost() - - @defer.inlineCallbacks - def tearDown(self): - from twisted.internet.tcp import Server - del self.ghost - yield self.svc.stopService() - # webkit has the bad habbit on not closing the persistent - # connections, so we need to hack them away to make trial happy - for reader in reactor.getReaders(): - if isinstance(reader, Server): - f = failure.Failure(Exception("test end")) - reader.connectionLost(f) - - @defer.inlineCallbacks +class TestGhostPy(www.WwwTestMixin,www.WwwGhostTestMixin, unittest.TestCase): def test_home(self): - yield self.ghost.open(self.url) - yield self.ghost.wait_for_selector("ul.breadcrumb") - base_url, resources = self.ghost.evaluate("bb_router.base_url") - assert(base_url== self.url) + return self.doPageLoadTest("/", + ( "tests.assertEqual(bb_router.base_url, '%(url)s')"%self.__dict__,)) + def test_doh_dojo_tests_colors(self): + """simple test to make sure our doh tester work. tests an already working dojo unit test""" + return self.doDohPageLoadRunnerTests() diff --git a/master/buildbot/test/util/txghost.py b/master/buildbot/test/util/txghost.py index 238f8e0871b..c4eb307549b 100644 --- a/master/buildbot/test/util/txghost.py +++ b/master/buildbot/test/util/txghost.py @@ -24,10 +24,10 @@ import time import codecs import json -import logging import subprocess from functools import wraps from twisted.internet import defer, reactor +from twisted.python import log try: import sip @@ -56,16 +56,6 @@ "(KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2" -logging.basicConfig() -logger = logging.getLogger('ghost') - - -class Logger(logging.Logger): - @staticmethod - def log(message, sender="Ghost", level="info"): - if not hasattr(logger, level): - raise Exception('invalid log level') - getattr(logger, level)("%s: %s", sender, message) class GhostWebPage(QtWebKit.QWebPage): @@ -80,14 +70,12 @@ def javaScriptConsoleMessage(self, message, line, source): """Prints client console message in current output stream.""" super(GhostWebPage, self).javaScriptConsoleMessage(message, line, source) - log_type = "error" if "Error" in message else "info" - Logger.log("%s(%d): %s" % (source or '', line, message), - sender="Frame", level=log_type) + log.msg("%s(%d): %s" % (source or '', line, message),system="Ghost:console") def javaScriptAlert(self, frame, message): """Notifies ghost for alert, then pass.""" Ghost._alert = message - Logger.log("alert('%s')" % message, sender="Frame") + log.msg("alert('%s')" % message,system="Ghost:Alert") def javaScriptConfirm(self, frame, message): """Checks if ghost is waiting for confirm, then returns the right @@ -98,7 +86,7 @@ def javaScriptConfirm(self, frame, message): message) confirmation, callback = Ghost._confirm_expected Ghost._confirm_expected = None - Logger.log("confirm('%s')" % message, sender="Frame") + log.msg("confirm('%s')" % message,system="Ghost:Confirm") if callback is not None: return callback() return confirmation @@ -111,12 +99,11 @@ def javaScriptPrompt(self, frame, message, defaultValue, result=None): raise Exception('You must specified a value for prompt "%s"' % message) result_value, callback = Ghost._prompt_expected - Logger.log("prompt('%s')" % message, sender="Frame") + log.msg("prompt('%s')" % message,system="Ghost:Prompt") if callback is not None: result_value = callback() if result_value == '': - Logger.log("'%s' prompt filled with empty string" % message, - level='warning') + log.msg("'%s' prompt filled with empty string" % message,system="Ghost:Prompt") Ghost._prompt_expected = None if result is None: # PySide @@ -171,7 +158,7 @@ def __init__(self, reply, cache): self.content = unicode(buffer.readAll(),"ascii","ignore") self.http_status = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) - Logger.log("Resource loaded: %s %s" % (self.url, self.http_status)) + log.msg("Resource loaded: %s %s" % (self.url, self.http_status),system="Ghost:Http") self.headers = {} for header in reply.rawHeaderList(): self.headers[unicode(header)] = unicode(reply.rawHeader(header)) @@ -196,7 +183,7 @@ class Ghost(object): _app = None def __init__(self, user_agent=default_user_agent, wait_timeout=8, - wait_callback=None, log_level=logging.WARNING, display=False, + wait_callback=None, display=False, viewport_size=(800, 600), cache_dir='/tmp/ghost.py'): self.http_resources = [] @@ -246,7 +233,6 @@ def __init__(self, user_agent=default_user_agent, wait_timeout=8, self.main_frame = self.page.mainFrame() - logger.setLevel(log_level) if self.display: self.webview = QtWebKit.QWebView() @@ -343,7 +329,16 @@ def evaluate(self, script): """ return (self.main_frame.evaluateJavaScript("%s" % script), self._release_last_resources()) - + def inject_script(self, script): + self.evaluate( + """(function(){e = document.createElement("script"); + e.type = "text/javascript"; + e.textContent = JSON.parse("%s") + e.charset = "utf-8"; + document.getElementsByTagName("head")[0].appendChild(e); + })() + """%(json.dumps(script).replace('"','\\"')) + ) def evaluate_js_file(self, path, encoding='utf-8'): """Evaluates javascript file at given path in current frame. Raises native IOException in case of invalid file. diff --git a/master/buildbot/test/util/www.py b/master/buildbot/test/util/www.py index 2d5872034fa..67935f2ec72 100644 --- a/master/buildbot/test/util/www.py +++ b/master/buildbot/test/util/www.py @@ -13,10 +13,16 @@ # # Copyright Buildbot Team Members +import os +from urlparse import urljoin from buildbot.util import json -from twisted.internet import defer +from twisted.internet import defer, reactor +from twisted.internet.error import CannotListenError +from twisted.internet.protocol import ServerFactory from twisted.web import server +from buildbot.www import service from buildbot.test.fake import fakemaster +from twisted.python import failure class FakeRequest(object): written = '' @@ -97,4 +103,112 @@ def assertRequest(self, content=None, contentJson=None, contentType=None, 'content-disposition') exp['contentDisposition'] = [ contentDisposition ] self.assertEqual(got, exp) - +try: + from buildbot.test.util.txghost import Ghost + has_ghost= Ghost != None +except ImportError: + no_ghost_message = "Need Ghost.py to run most of www_ui tests" + has_ghost=False + # if $REQUIRE_GHOST is set, then fail if it's not found + if os.environ.get('REQUIRE_GHOST'): + raise + +if has_ghost: + PUBLIC_HTML_PATH = os.environ.get('PUBLIC_HTML_PATH') + if not PUBLIC_HTML_PATH: + has_ghost=False + no_ghost_message = ("Need PUBLIC_HTML_PATH environment variable which points on 'updatejs'" + " installed directory") + PUBLIC_HTML_PATH = os.path.abspath(PUBLIC_HTML_PATH) + if (not os.path.isdir(PUBLIC_HTML_PATH) or + not os.path.isfile(os.path.join(PUBLIC_HTML_PATH,"static","js.built","dojo","dojo.js"))): + has_ghost=False + no_ghost_message = ("Needs PUBLIC_HTML_PATH environment variable which points on 'updatejs'" + " installed directory, but got" + PUBLIC_HTML_PATH) + +if not has_ghost and os.environ.get('REQUIRE_GHOST'): + raise Exception(no_ghost_message) + + +class WwwGhostTestMixin(object): + if not has_ghost: + skip = no_ghost_message + + @defer.inlineCallbacks + def setUp(self): + # hack to prevent twisted.web.http to setup a 1 sec callback at init + import twisted + #twisted.internet.base.DelayedCall.debug = True + twisted.web.http._logDateTimeUsers = 1 + portFound = False + port = 18010 + while not portFound: + try: + tcp = reactor.listenTCP(port, ServerFactory()) + portFound = True + yield tcp.stopListening() + except CannotListenError: + port+=1 + self.url = 'http://localhost:'+str(port)+"/" + self.master = self.make_master(url=self.url, port=port, public_html= PUBLIC_HTML_PATH) + self.svc = service.WWWService(self.master) + yield self.svc.startService() + yield self.svc.reconfigService(self.master.config) + self.ghost = Ghost() + + @defer.inlineCallbacks + def tearDown(self): + from twisted.internet.tcp import Server + del self.ghost + yield self.svc.stopService() + # webkit has the bad habbit on not closing the persistent + # connections, so we need to hack them away to make trial happy + for reader in reactor.getReaders(): + if isinstance(reader, Server): + f = failure.Failure(Exception("test end")) + reader.connectionLost(f) + + @defer.inlineCallbacks + def doPageLoadTest(self, ui_path, js_assertions, selector_to_wait = "#content div"): + """ start ghost on the given path, and make a bunch of js assertions""" + yield self.ghost.open(urljoin(self.url,ui_path)) + yield self.ghost.wait_for_selector(selector_to_wait) + # import test framework in the global namespace + # This is a kind of tricky hack in order to load doh without the _browserRunner module + # at the end of runner.js, there is a legacy hack to load also _browserRunner, which + # does much more than we need, including hooking console.log + runner = open(os.path.join(PUBLIC_HTML_PATH, "static","js","util","doh","runner.js")).read() + runner = "\n".join(runner.splitlines()[:-4]) + runner = runner.replace('define("doh/runner",', "require(") + runner = runner.replace('return doh;', + """var div = document.createElement("div"); + div.innerHTML = "
"; + document.body.appendChild(div); + return doh;""") + self.ghost.evaluate(runner) + yield self.ghost.wait_for_selector("#doh_loaded") + doh_boilerplate = """ + (function() { + try { + %(js)s + } catch(err) { + return String(err); + } return "OK"; + })() + """ + for js in js_assertions: + result, _ = self.ghost.evaluate(doh_boilerplate%dict(js=js)) + self.assertEqual(result, "OK") + + @defer.inlineCallbacks + def doDohPageLoadRunnerTests(self, doh_tests="dojo/tests/colors"): + self.ghost.wait_timeout = 200 + yield self.ghost.open(urljoin(self.url,"static/js.built/lib/tests/runner.html")) + result_selector = "#testListContainer table tfoot tr.inProgress" + self.ghost.inject_script("require(['dojo', 'doh', 'dojo/window'], function(dojo,doh){ require(['"+doh_tests+"'], function(){doh.run()})});") + result_selector = "#testListContainer table tfoot tr.inProgress" + yield self.ghost.wait_for_selector(result_selector) + result, _ = self.ghost.evaluate("dojo.map(dojo.query('"+result_selector+" .failure'),function(a){return a.textContent;});") + self.assertEqual(result, ['0','0']) + result, _ = self.ghost.evaluate("dojo.map(dojo.query('"+result_selector+" td'),function(a){return a.textContent;});") + print "\n",str(result[1]).strip(), diff --git a/master/buildbot/www/service.py b/master/buildbot/www/service.py index 92813832c98..b8692754253 100644 --- a/master/buildbot/www/service.py +++ b/master/buildbot/www/service.py @@ -13,11 +13,13 @@ # # Copyright Buildbot Team Members -from twisted.internet import defer +import os, shutil +from twisted.internet import defer, threads from twisted.python import util from twisted.application import strports, service from twisted.web import server, static from buildbot import config +from buildbot.util import json from buildbot.www import ui, resource, rest, ws class WWWService(config.ReconfigurableServiceMixin, service.MultiService): @@ -32,13 +34,13 @@ def __init__(self, master): self.site = None self.site_public_html = None - @defer.deferredGenerator + @defer.inlineCallbacks def reconfigService(self, new_config): www = new_config.www need_new_site = False if self.site: - if www.get('public_html') != self.site_public_html: + if www.get('public_html') != self.site_public_html or www.get('extra_js',[]) != self.site_extra_js: need_new_site = True else: if www['port']: @@ -49,11 +51,8 @@ def reconfigService(self, new_config): if www['port'] != self.port: if self.port_service: - wfd = defer.waitForDeferred( - defer.maybeDeferred(lambda : - self.port_service.disownServiceParent())) - yield wfd - wfd.getResult() + yield defer.maybeDeferred(lambda : + self.port_service.disownServiceParent()) self.port_service = None self.port = www['port'] @@ -64,25 +63,29 @@ def reconfigService(self, new_config): self.port_service = strports.service(port, self.site) self.port_service.setServiceParent(self) - wfd = defer.waitForDeferred( - config.ReconfigurableServiceMixin.reconfigService(self, - new_config)) - yield wfd - wfd.getResult() + yield config.ReconfigurableServiceMixin.reconfigService(self, + new_config) def setup_site(self, new_config): - public_html = self.site_public_html = new_config.www.get('public_html') - if public_html: - root = static.File(public_html) - else: - root = static.Data('placeholder', 'text/plain') + extra_js = self.site_extra_js = new_config.www.get('extra_js', []) + extra_routes = [] + for js in extra_js: + js = os.path.join(public_html, "static", "js", os.path.basename(js)) + if not os.path.isdir(js): + raise ValueError("missing js files in %s: please do buildbot upgrade_master" + " or update_js"%(js,)) + if os.path.exists(os.path.join(js, "routes.js")): + extra_routes.append(os.path.basename(js)+"/routes") + + + root = static.File(public_html) # redirect the root to UI root.putChild('', resource.RedirectResource(self.master, 'ui/')) # /ui - root.putChild('ui', ui.UIResource(self.master)) + root.putChild('ui', ui.UIResource(self.master, extra_routes)) # /api root.putChild('api', rest.RestRootResource(self.master)) @@ -90,8 +93,5 @@ def setup_site(self, new_config): # /ws root.putChild('ws', ws.WsResource(self.master)) - # /static - staticdir = util.sibpath(__file__, 'static') - root.putChild('static', static.File(staticdir)) - self.site = server.Site(root) + diff --git a/master/buildbot/www/static/css/default.css b/master/buildbot/www/static/css/default.css index 645a39cf99a..7225e973a99 100644 --- a/master/buildbot/www/static/css/default.css +++ b/master/buildbot/www/static/css/default.css @@ -56,11 +56,10 @@ body, html { height:100% } #loading{ -height:80%; +position:fixed; +top:100px; } #loading_anim{ -top:40%; -left:50%; position:relative; width:62px; height:77px} @@ -234,3 +233,4 @@ background-color:#000000} background-color:#FFFFFF} } + diff --git a/master/buildbot/www/static/js/buildbot.js b/master/buildbot/www/static/js/buildbot.js deleted file mode 100644 index a6d9b4021c2..00000000000 --- a/master/buildbot/www/static/js/buildbot.js +++ /dev/null @@ -1,29 +0,0 @@ -require({ - packages: [{name: "lib", location: "/static/js/lib"}] -}, - [ - "lib/websocket", - "lib/changes", - "dojo/on", - "dojo/dom", - "dojo/json", - "dojo/html", - "dojo/_base/fx", - ], - - function(websocket, changes, on, dom, JSON, fx, html) { - setTimeout(websocket.startWebsocket, 0); - - //Add onClick event for changes - var changesButton = dom.byId("changesButton"); - on(changesButton, "click", function(evt){ - data = changes.loadSingleChange(); - data.then(function(d) { - var content = dom.byId("changesContent"); - // html.set(dom.byId("content"), JSON.stringify(d)); - content.innerHTML += JSON.stringify(d); - }); - }); - }); - - diff --git a/master/buildbot/www/static/js/lib/fakeStore.js b/master/buildbot/www/static/js/lib/fakeStore.js index dfb2098f510..a51dc74131f 100644 --- a/master/buildbot/www/static/js/lib/fakeStore.js +++ b/master/buildbot/www/static/js/lib/fakeStore.js @@ -6,9 +6,8 @@ define( function(declare, Memory) { return declare([Memory], { fields: ["field1"], - constructor: function(args){ - declare.safeMixin(this,args); - this.interval = setInterval(dojo.hitch(this, this.addSomeData), 1000); //simulate adding some data every second + autoUpdate: function(args){ + this.interval = setInterval(dojo.hitch(this, this.addSomeData), 1000); //simulate adding some data every second }, addSomeData: function() { var randomfeed = "sdlkjs alkdj alsdjl ksdj lsajldkjaslkdj asdlkja iwjedo ajlskj lhsl"; @@ -22,7 +21,7 @@ define( var l = Math.floor(Math.random()*20); var v = ""; for (var j=0; j/g, ">"). - replace(/\"/g, """); - } - - function render_attribs(attribs) { - var key, value, result = []; - for (key in attribs) { - if (key !== '_content' && attribs.hasOwnProperty(key)) { - switch (attribs[key]) { - case 'undefined': - case 'false': - case 'null': - case '""': - break; - default: - try { - value = JSON.parse("[" + attribs[key] +"]")[0]; - if (value === true) { - value = key; - } else if (typeof value === 'string' && embedder.test(value)) { - value = '" +\n' + parse_interpol(html_escape(value)) + ' +\n"'; - } else { - value = html_escape(value); - } - result.push(" " + key + '=\\"' + value + '\\"'); - } catch (e) { - result.push(" " + key + '=\\"" + '+escaperName+'(' + attribs[key] + ') + "\\"'); - } - } - } - } - return result.join(""); - } - - // Parse the attribute block using a state machine - function parse_attribs(line) { - var attributes = {}, - l = line.length, - i, c, - count = 1, - quote = false, - skip = false, - open, close, joiner, seperator, - pair = { - start: 1, - middle: null, - end: null - }; - - if (!(l > 0 && (line.charAt(0) === '{' || line.charAt(0) === '('))) { - return { - _content: line[0] === ' ' ? line.substr(1, l) : line - }; - } - open = line.charAt(0); - close = (open === '{') ? '}' : ')'; - joiner = (open === '{') ? ':' : '='; - seperator = (open === '{') ? ',' : ' '; - - function process_pair() { - if (typeof pair.start === 'number' && - typeof pair.middle === 'number' && - typeof pair.end === 'number') { - var key = line.substr(pair.start, pair.middle - pair.start).trim(), - value = line.substr(pair.middle + 1, pair.end - pair.middle - 1).trim(); - attributes[key] = value; - } - pair = { - start: null, - middle: null, - end: null - }; - } - - for (i = 1; count > 0; i += 1) { - - // If we reach the end of the line, then there is a problem - if (i > l) { - throw "Malformed attribute block"; - } - - c = line.charAt(i); - if (skip) { - skip = false; - } else { - if (quote) { - if (c === '\\') { - skip = true; - } - if (c === quote) { - quote = false; - } - } else { - if (c === '"' || c === "'") { - quote = c; - } - - if (count === 1) { - if (c === joiner) { - pair.middle = i; - } - if (c === seperator || c === close) { - pair.end = i; - process_pair(); - if (c === seperator) { - pair.start = i + 1; - } - } - } - - if (c === open || c === "(") { - count += 1; - } - if (c === close || (count > 1 && c === ")")) { - count -= 1; - } - } - } - } - attributes._content = line.substr(i, line.length); - return attributes; - } - - // Split interpolated strings into an array of literals and code fragments. - function parse_interpol(value) { - var items = [], - pos = 0, - next = 0, - match; - while (true) { - // Match up to embedded string - next = value.substr(pos).search(embedder); - if (next < 0) { - if (pos < value.length) { - items.push(JSON.stringify(value.substr(pos))); - } - break; - } - items.push(JSON.stringify(value.substr(pos, next))); - pos += next; - - // Match embedded string - match = value.substr(pos).match(embedder); - next = match[0].length; - if (next < 0) { break; } - if(match[1] === "#"){ - items.push(escaperName+"("+(match[2] || match[3])+")"); - }else{ - //unsafe!!! - items.push(match[2] || match[3]); - } - - pos += next; - } - return items.filter(function (part) { return part && part.length > 0}).join(" +\n"); - } - - // Used to find embedded code in interpolated strings. - embedder = /([#!])\{([^}]*)\}/; - - self_close_tags = ["meta", "img", "link", "br", "hr", "input", "area", "base"]; - - // All matchers' regexps should capture leading whitespace in first capture - // and trailing content in last capture - matchers = [ - // html tags - { - name: "html tags", - regexp: /^(\s*)((?:[.#%][a-z_\-][a-z0-9_:\-]*)+)(.*)$/i, - process: function () { - var line_beginning, tag, classes, ids, attribs, content, whitespaceSpecifier, whitespace={}, output; - line_beginning = this.matches[2]; - classes = line_beginning.match(/\.([a-z_\-][a-z0-9_\-]*)/gi); - ids = line_beginning.match(/\#([a-z_\-][a-z0-9_\-]*)/gi); - tag = line_beginning.match(/\%([a-z_\-][a-z0-9_:\-]*)/gi); - - // Default to
tag - tag = tag ? tag[0].substr(1, tag[0].length) : 'div'; - - attribs = this.matches[3]; - if (attribs) { - attribs = parse_attribs(attribs); - if (attribs._content) { - var leader0 = attribs._content.charAt(0), - leader1 = attribs._content.charAt(1), - leaderLength = 0; - - if(leader0 == "<"){ - leaderLength++; - whitespace.inside = true; - if(leader1 == ">"){ - leaderLength++; - whitespace.around = true; - } - }else if(leader0 == ">"){ - leaderLength++; - whitespace.around = true; - if(leader1 == "<"){ - leaderLength++; - whitespace.inside = true; - } - } - attribs._content = attribs._content.substr(leaderLength); - //once we've identified the tag and its attributes, the rest is content. - // this is currently trimmed for neatness. - this.contents.unshift(attribs._content.trim()); - delete(attribs._content); - } - } else { - attribs = {}; - } - - if (classes) { - classes = classes.map(function (klass) { - return klass.substr(1, klass.length); - }).join(' '); - if (attribs['class']) { - try { - attribs['class'] = JSON.stringify(classes + " " + JSON.parse(attribs['class'])); - } catch (e) { - attribs['class'] = JSON.stringify(classes + " ") + " + " + attribs['class']; - } - } else { - attribs['class'] = JSON.stringify(classes); - } - } - if (ids) { - ids = ids.map(function (id) { - return id.substr(1, id.length); - }).join(' '); - if (attribs.id) { - attribs.id = JSON.stringify(ids + " ") + attribs.id; - } else { - attribs.id = JSON.stringify(ids); - } - } - - attribs = render_attribs(attribs); - - content = this.render_contents(); - if (content === '""') { - content = ''; - } - - if(whitespace.inside){ - if(content.length==0){ - content='" "' - }else{ - try{ //remove quotes if they are there - content = '" '+JSON.parse(content)+' "'; - }catch(e){ - content = '" "+\n'+content+'+\n" "'; - } - } - } - - if (forceXML ? content.length > 0 : self_close_tags.indexOf(tag) == -1) { - output = '"<' + tag + attribs + '>"' + - (content.length > 0 ? ' + \n' + content : "") + - ' + \n""'; - } else { - output = '"<' + tag + attribs + ' />"'; - } - - if(whitespace.around){ - //output now contains '"hello"' - //we need to crack it open to insert whitespace. - output = '" '+output.substr(1, output.length - 2)+' "'; - } - - return output; - } - }, - - // each loops - { - name: "each loop", - regexp: /^(\s*)(?::for|:each)\s+(?:([a-z_][a-z_\-]*),\s*)?([a-z_][a-z_\-]*)\s+in\s+(.*)(\s*)$/i, - process: function () { - var ivar = this.matches[2] || '__key__', // index - vvar = this.matches[3], // value - avar = this.matches[4], // array - rvar = '__result__'; // results - - if (this.matches[5]) { - this.contents.unshift(this.matches[5]); - } - return '(function () { ' + - 'var ' + rvar + ' = [], ' + ivar + ', ' + vvar + '; ' + - 'for (' + ivar + ' in ' + avar + ') { ' + - 'if (' + avar + '.hasOwnProperty(' + ivar + ')) { ' + - vvar + ' = ' + avar + '[' + ivar + ']; ' + - rvar + '.push(\n' + (this.render_contents() || "''") + '\n); ' + - '} } return ' + rvar + '.join(""); }).call(this)'; - } - }, - - // if statements - { - name: "if", - regexp: /^(\s*):if\s+(.*)\s*$/i, - process: function () { - var condition = this.matches[2]; - return '(function () { ' + - 'if (' + condition + ') { ' + - 'return (\n' + (this.render_contents() || '') + '\n);' + - '} else { return ""; } }).call(this)'; - } - }, - - // silent-comments - { - name: "silent-comments", - regexp: /^(\s*)-#\s*(.*)\s*$/i, - process: function () { - return '""'; - } - }, - - //html-comments - { - name: "silent-comments", - regexp: /^(\s*)\/\s*(.*)\s*$/i, - process: function () { - this.contents.unshift(this.matches[2]); - - return '""'; - } - }, - - // raw js - { - name: "rawjs", - regexp: /^(\s*)-\s*(.*)\s*$/i, - process: function () { - this.contents.unshift(this.matches[2]); - return '"";' + this.contents.join("\n")+"; _$output = _$output "; - } - }, - - // raw js - { - name: "pre", - regexp: /^(\s*):pre(\s+(.*)|$)/i, - process: function () { - this.contents.unshift(this.matches[2]); - return '"
"+\n' + JSON.stringify(this.contents.join("\n"))+'+\n"
"'; - } - }, - - // declarations - { - name: "doctype", - regexp: /^()!!!(?:\s*(.*))\s*$/, - process: function () { - var line = ''; - switch ((this.matches[2] || '').toLowerCase()) { - case '': - // XHTML 1.0 Transitional - line = ''; - break; - case 'strict': - case '1.0': - // XHTML 1.0 Strict - line = ''; - break; - case 'frameset': - // XHTML 1.0 Frameset - line = ''; - break; - case '5': - // XHTML 5 - line = ''; - break; - case '1.1': - // XHTML 1.1 - line = ''; - break; - case 'basic': - // XHTML Basic 1.1 - line = ''; - break; - case 'mobile': - // XHTML Mobile 1.2 - line = ''; - break; - case 'xml': - // XML - line = ""; - break; - case 'xml iso-8859-1': - // XML iso-8859-1 - line = ""; - break; - } - return JSON.stringify(line + "\n"); - } - }, - - // Embedded markdown. Needs to be added to exports externally. - { - name: "markdown", - regexp: /^(\s*):markdown\s*$/i, - process: function () { - return parse_interpol(exports.Markdown.encode(this.contents.join("\n"))); - } - }, - - // script blocks - { - name: "script", - regexp: /^(\s*):(?:java)?script\s*$/, - process: function () { - return parse_interpol('\n\n"); - } - }, - - // css blocks - { - name: "css", - regexp: /^(\s*):css\s*$/, - process: function () { - return JSON.stringify('"); - } - } - - ]; - - function compile(lines) { - var block = false, - output = []; - - // If lines is a string, turn it into an array - if (typeof lines === 'string') { - lines = lines.trim().replace(/\n\r|\r/g, '\n').split('\n'); - } - - lines.forEach(function(line) { - var match, found = false; - - // Collect all text as raw until outdent - if (block) { - match = block.check_indent.exec(line); - if (match) { - block.contents.push(match[1] || ""); - return; - } else { - output.push(block.process()); - block = false; - } - } - - matchers.forEach(function (matcher) { - if (!found) { - match = matcher.regexp.exec(line); - if (match) { - block = { - contents: [], - indent_level: (match[1]), - matches: match, - check_indent: new RegExp("^(?:\\s*|" + match[1] + " (.*))$"), - process: matcher.process, - render_contents: function () { - return compile(this.contents); - } - }; - found = true; - } - } - }); - - // Match plain text - if (!found) { - output.push(function () { - // Escaped plain text - if (line[0] === '\\') { - return parse_interpol(line.substr(1, line.length)); - } - - - function escapedLine(){ - try { - return escaperName+'('+JSON.stringify(JSON.parse(line)) +')'; - } catch (e2) { - return escaperName+'(' + line + ')'; - } - } - - function unescapedLine(){ - try { - return parse_interpol(JSON.parse(line)); - } catch (e) { - return line; - } - } - - // always escaped - if((line.substr(0, 2) === "&=")) { - line = line.substr(2, line.length).trim(); - return escapedLine(); - } - - //never escaped - if((line.substr(0, 2) === "!=")) { - line = line.substr(2, line.length).trim(); - return unescapedLine(); - } - - // sometimes escaped - if ( (line[0] === '=')) { - line = line.substr(1, line.length).trim(); - if(escapeHtmlByDefault){ - return escapedLine(); - }else{ - return unescapedLine(); - } - } - - // Plain text - return parse_interpol(line); - }()); - } - - }); - if (block) { - output.push(block.process()); - } - - var txt = output.filter(function (part) { return part && part.length > 0}).join(" +\n"); - if(txt.length == 0){ - txt = '""'; - } - return txt; - }; - - function optimize(js) { - var new_js = [], buffer = [], part, end; - - function flush() { - if (buffer.length > 0) { - new_js.push(JSON.stringify(buffer.join("")) + end); - buffer = []; - } - } - js.replace(/\n\r|\r/g, '\n').split('\n').forEach(function (line) { - part = line.match(/^(\".*\")(\s*\+\s*)?$/); - if (!part) { - flush(); - new_js.push(line); - return; - } - end = part[2] || ""; - part = part[1]; - try { - buffer.push(JSON.parse(part)); - } catch (e) { - flush(); - new_js.push(line); - } - }); - flush(); - return new_js.join("\n"); - }; - - function render(text, options) { - options = options || {}; - text = text || ""; - var js = compile(text, options); - if (options.optimize) { - js = Haml.optimize(js); - } - return execute(js, options.context || Haml, options.locals); - }; - - function execute(js, self, locals) { - return (function () { - with(locals || {}) { - try { - var _$output; - eval("_$output =" + js ); - return _$output; //set in eval - } catch (e) { - return "\n
" + html_escape(e.stack) + "
\n"; - } - - } - }).call(self); - }; - - Haml = function Haml(haml, config) { - if(typeof(config) != "object"){ - forceXML = config; - config = {}; - } - - var escaper; - if(config.customEscape){ - escaper = ""; - escaperName = config.customEscape; - }else{ - escaper = html_escape.toString() + "\n"; - escaperName = "html_escape"; - } - - escapeHtmlByDefault = (config.escapeHtmlByDefault || config.escapeHTML || config.escape_html); - - var js = optimize(compile(haml)); - - var str = "with(locals || {}) {\n" + - " try {\n" + - " var _$output=" + js + ";\n return _$output;" + - " } catch (e) {\n" + - " return \"\\n
\" + "+escaperName+"(e.stack) + \"
\\n\";\n" + - " }\n" + - "}" - - try{ - var f = new Function("locals", escaper + str ); - return f; - }catch(e){ - if ( typeof(console) !== 'undefined' ) { console.log(str); console.error(e); } - return function() { - return "

Error in parsing haml!

"+Haml.html_escape(str)+"

"+Haml.html_escape(e.toString())+"
"; - } - } - } - /* dojo/AMD magic: require( ["lib/haml!./template/mytemplate.haml"], function(template){ ... - */ - function load(resourceDef, require, callback, config) { - var url = require.toUrl(resourceDef); - if(hamlCache[url]){ - return hamlCache[url]; - } - xhr("GET", {url:url, sync:!require.async, load:function(text) { - var js = Haml(text, config); - hamlCache[url] = js; - callback(js); - }}); - } - - Haml.compile = compile; - Haml.optimize = optimize; - Haml.render = render; - Haml.execute = execute; - Haml.html_escape = html_escape; - Haml.load = load; - return Haml; -}); diff --git a/master/buildbot/www/static/js/lib/router.js b/master/buildbot/www/static/js/lib/router.js index d804eeedd9b..e7043ea5b03 100644 --- a/master/buildbot/www/static/js/lib/router.js +++ b/master/buildbot/www/static/js/lib/router.js @@ -13,8 +13,8 @@ // // Copyright Buildbot Team Members -define(["dojo/_base/declare", "dojo/_base/connect","dojo/_base/array","dojo/dom", "put-selector/put","dojo/hash", "dojo/io-query", "dojo/dom-class"], - function(declare, connect, array, dom, put, hash, ioquery, domclass) { +define(["dojo/_base/declare", "dojo/_base/connect","dojo/_base/array","dojo/dom", "put-selector/put","dojo/hash", "dojo/io-query", "dojo/dom-class", "dojo/window"], + function(declare, connect, array, dom, put, hash, ioquery, domclass, win) { "use strict"; /* allow chrome to display correctly generic errbacks from dojo */ console.error = function(err) { @@ -113,22 +113,21 @@ define(["dojo/_base/declare", "dojo/_base/connect","dojo/_base/array","dojo/dom" /* make sure we give user feedback in case of malformated args */ hash("/"+path+reformated_args); require(["dijit/dijit", widget], function(dijit, Widget) { - var old = dijit.byId("content"); - if (old) { - old.destroy(); - dijit.registry.remove("content"); - } - var w = new Widget({id:"content",path_components:match, url_args:args}); - var content = dojo.byId("content"); - content.innerHTML = ""; + var w = new Widget({path_components:match, url_args:args}); var loading = dojo.byId("loading"); loading.style.display = "block"; - content.innerHTML = ""; - + loading.style.left = (((win.getBox().w+loading.offsetWidth)/2))+"px"; dojo.when(w.readyDeferred, function() { - loading.style.display = "none"; + var old = window.bb.curWidget; + if (old) { + old.destroy(); + dijit.registry.remove(old.id); + } + var content = dojo.byId("content"); content.innerHTML = ""; + loading.style.display = "none"; w.placeAt(content); + window.bb.curWidget = w; }); }); if (nav) { @@ -163,8 +162,9 @@ define(["dojo/_base/declare", "dojo/_base/connect","dojo/_base/array","dojo/dom" return _default; } var json = localStorage.getItem(key); - if (json === null) + if (json === null){ return _default; + } return dojo.fromJson(json); }, addHistory: function(history_type, title) { @@ -180,7 +180,7 @@ define(["dojo/_base/declare", "dojo/_base/connect","dojo/_base/array","dojo/dom" if (recent_stuff.length >= 5) { recent_stuff.pop(); } - recent_stuff.unshift({url:"#"+hash(), title:title}) + recent_stuff.unshift({url:"#"+hash(), title:title}); this.localStore(history_type, recent_stuff); }, reload: function(){ diff --git a/master/buildbot/www/static/js/lib/tests/runner.html b/master/buildbot/www/static/js/lib/tests/runner.html new file mode 100644 index 00000000000..7f799a2e136 --- /dev/null +++ b/master/buildbot/www/static/js/lib/tests/runner.html @@ -0,0 +1,250 @@ + + + + The Dojo Unit Test Harness, $Rev: 29130 $ + + + + + + + + + + + + + + + + + + + + +
+

D.O.H.: The Dojo Objective Harness

+ + + + + +
+ + + + + + Stopped + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
  + + testtime
+
+
+
+
+

+							
+
+ + +
+
+ + + + diff --git a/master/buildbot/www/static/js/lib/ui/base.js b/master/buildbot/www/static/js/lib/ui/base.js index b81204a7f5e..1101418a5c0 100644 --- a/master/buildbot/www/static/js/lib/ui/base.js +++ b/master/buildbot/www/static/js/lib/ui/base.js @@ -15,7 +15,7 @@ define(["dojo/_base/declare", "dijit/_Widget", "dijit/_TemplatedMixin", "dijit/_WidgetsInTemplateMixin", "dojo/_base/Deferred", "dojo/_base/xhr", - "lib/haml!./templates/error.haml"], + "./templates/error.haml"], function(declare, _Widget, _TemplatedMixin, _WidgetsInTemplateMixin, Deferred, xhr, template) { return declare([_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], { templateFunc: template, @@ -56,14 +56,19 @@ define(["dojo/_base/declare", "dijit/_Widget", "dijit/_TemplatedMixin", "dijit/_ this.error_msg = text; this.templateFunc = template; /* restore the error template that was overiden by childrens*/ }, - getApiV1: function() { - var a = arguments; + createAPIPath: function(a) { var path=[]; - for (var i = 0;i0) || diff --git a/master/buildbot/www/static/js/lib/ui/builder.js b/master/buildbot/www/static/js/lib/ui/builder.js index 749977caa1f..d13456c0b6b 100644 --- a/master/buildbot/www/static/js/lib/ui/builder.js +++ b/master/buildbot/www/static/js/lib/ui/builder.js @@ -14,10 +14,10 @@ // Copyright Buildbot Team Members define(["dojo/_base/declare", "lib/ui/base", - "dgrid/OnDemandGrid", "dojo/store/Observable", "dojo/store/Memory", + "dojo/store/Observable", "dojo/store/Memory", "dojo/_base/array", - "lib/haml!./templates/builder.haml" -], function(declare, Base, Grid, observable, Memory, array, template) { + "./templates/builder.haml" +], function(declare, Base, observable, Memory, array, template) { "use strict"; return declare([Base], { templateFunc:template, @@ -56,10 +56,11 @@ define(["dojo/_base/declare", "lib/ui/base", } } var store = observable(new Memory({data:data,idProperty: "number"})); - var grid = new (declare([Grid]))({ + this.createBaseGrid({ store: store, cellNavigation:false, tabableHeader: false, + contentMaxHeight:700, columns: { number: {label:"number", get: function(o){return o;}, @@ -70,6 +71,12 @@ define(["dojo/_base/declare", "lib/ui/base", reason: "Reason", slave: "Slave", owner: "Owner", + started: {label:"Started", + get: function(o){return o.times[0];}, + type:"date"}, + ended: {label:"Finished", + get: function(o){return o.times[1];}, + type:"date"}, text: {label:"results", get: function(o){return o;}, formatter: function(data){ @@ -79,7 +86,6 @@ define(["dojo/_base/declare", "lib/ui/base", } } }, this.buildersgrid_node); - grid.refresh(); } }); }); diff --git a/master/buildbot/www/static/js/lib/ui/builders.js b/master/buildbot/www/static/js/lib/ui/builders.js index 41cb74834c6..d406e511208 100644 --- a/master/buildbot/www/static/js/lib/ui/builders.js +++ b/master/buildbot/www/static/js/lib/ui/builders.js @@ -14,10 +14,10 @@ // Copyright Buildbot Team Members define(["dojo/_base/declare", "lib/ui/base", - "dgrid/OnDemandGrid", "dojo/store/Observable", "dojo/store/Memory", + "dojo/store/Observable", "dojo/store/Memory", "dojo/_base/array", - "lib/haml!./templates/builders.haml" - ], function(declare, Base, Grid, observable, Memory, array, template) { + "./templates/builders.haml" + ], function(declare, Base, observable, Memory, array, template) { "use strict"; return declare([Base], { templateFunc : template, @@ -38,14 +38,15 @@ define(["dojo/_base/declare", "lib/ui/base", } } var store = observable(new Memory({data:data,idProperty: "builderName"})); - var grid = new (declare([Grid]))({ + this.createBaseGrid({ store: store, cellNavigation:false, tabableHeader: false, + contentMaxHeight:700, columns: { builderName: {label:"BuilderName", formatter: function(b) { - return ""+b+""; + return ""+b+""; }}, slaves: {label:"Slaves", formatter: function(s) { @@ -58,7 +59,7 @@ define(["dojo/_base/declare", "lib/ui/base", formatter: function(data) { return array.map(data.currentBuilds, function(s) { - return ""+s+""; + return ""+s+""; }).join(","); }}, state: {label:"Status", @@ -73,7 +74,6 @@ define(["dojo/_base/declare", "lib/ui/base", } } }, this.buildersgrid_node); - grid.refresh(); } }); }); diff --git a/master/buildbot/www/static/js/lib/ui/buildmasters.js b/master/buildbot/www/static/js/lib/ui/buildmasters.js index 909c330a331..55c1536be5d 100644 --- a/master/buildbot/www/static/js/lib/ui/buildmasters.js +++ b/master/buildbot/www/static/js/lib/ui/buildmasters.js @@ -13,11 +13,24 @@ // // Copyright Buildbot Team Members -define(["dojo/_base/declare", "lib/ui/base"], function(declare, Base) { +define(["dojo/_base/declare", "lib/ui/base", + "./templates/buildmasters.haml" + ], function(declare, Base, template) { "use strict"; return declare([Base], { - constructor: function(args){ - declare.safeMixin(this,args); - } + templateFunc : template, + postCreate: function(){ + this.createGrid({ + apiPath : "master", + idProperty: "masterid", + columns: { + "masterid": { label:"#",style:"width:30px"}, + "name": { label:"Name"}, + "link": { label:"Json Link", type:"url"}, + "last_active": { label:"Last Active", type:"date"}, + "active": { label:"Active",type:"bool"} + } + }, this.buildmastersgrid_node); + } }); }); diff --git a/master/buildbot/www/static/js/lib/ui/changes.js b/master/buildbot/www/static/js/lib/ui/changes.js index 5b8f94c2df4..160c2c03378 100644 --- a/master/buildbot/www/static/js/lib/ui/changes.js +++ b/master/buildbot/www/static/js/lib/ui/changes.js @@ -13,56 +13,27 @@ // // Copyright Buildbot Team Members -define(["dojo/_base/declare", "lib/ui/base", "dgrid/OnDemandGrid", - "dgrid/Selection", "dgrid/Keyboard","dgrid/extensions/ColumnHider", - "dojo/store/Observable", "lib/fakeChangeStore", - "lib/haml!./templates/changes.haml"], - function(declare, Base, Grid, Selection, Keyboard, Hider, observable, Store, template) { - "use strict"; - return declare([Base], { - templateFunc:template, - constructor: function(args){ - declare.safeMixin(this,args); - }, - postCreate: function(){ - this.store = observable(new Store()); - var maingrid = new (declare([Grid, Selection, Keyboard, Hider]))({ - loadingMessage: "loading...", - store: this.store, - minRowsPerPage: 15, - maxRowsPerPage: 15, - cellNavigation:false, - tabableHeader: false, - columns: { - id: {label:"ID", width:"10px"}, - changeid: "Change ID", - revision: - {label: "Revision", - sortable: false}, - committer: - {label: "committer", - sortable: false}, - comments: - {label: "Comments", - sortable: false} - } - }, this.maingrid_node); - maingrid.on(".dgrid-row:dblclick", dojo.hitch(this, this.rowDblClick)); - maingrid.on("dgrid-select", dojo.hitch(this, this.select)); - maingrid.refresh(); - this.maingrid = maingrid; - }, - select: function(event) { - var _this = this; - if (this.withSelect) { - this.withSelect(arrayUtil.map(event.rows, function(row){ return _this.store.get(row.id); })); - } - }, - rowDblClick : function(evt) { - }, - destroy: function(){ - this.maingrid.destroy(); - this.store.destroy(); - } - }); - }); +define(["dojo/_base/declare", "lib/ui/base", + "./templates/changes.haml" + ], function(declare, Base, template) { + "use strict"; + return declare([Base], { + templateFunc : template, + postCreate: function(){ + this.createGrid({ + apiPath : "change", + idProperty: "changeid", + contentMaxHeight:700, + columns: { + "changeid": { label:"#",style:"width:30px"}, + "author": { label:"Author",type:"user"}, + "branch": { label:"Branch",style:"width:100px"}, + "repository": { label:"Repository", type:"url"}, + "revision": { label:"Revision", type:"revision"}, + "when_timestamp": { label:"Date",type:"date"}, + "files": { label:"Files",type:"filelist"} + } + }, this.maingrid_node); + } + }); +}); diff --git a/master/buildbot/www/static/js/lib/ui/dgridext/AutoHeight.js b/master/buildbot/www/static/js/lib/ui/dgridext/AutoHeight.js new file mode 100644 index 00000000000..954f3a03813 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/dgridext/AutoHeight.js @@ -0,0 +1,62 @@ +define(["dojo/_base/declare", "dojo/_base/array","dojo/has","dojo/_base/html"], +function(declare, array, has){ + var updateHeightAndInherit = function () { + this.updateHeight(); + return this.inherited(arguments); + }; + + return declare(null, { + contentMaxHeight : 200, + _willUpdateHeight : null, + removeRow: function () { + this.updateHeight(); + return this.inherited(arguments); + }, + insertRow: function () { + this.updateHeight(); + return this.inherited(arguments); + }, + styleColumn: function () { + this.updateHeight(); + return this.inherited(arguments); + }, + updateHeight:function(){ + var self = this; + + if (has("ie") < 8 || has("quirks")) { + /* ie non standard mode does not like this method */ + return; + } + if (self._willUpdateHeight === null) { + self._willUpdateHeight = setTimeout(function(){ + function heightFromChildrens(dom, max) { + var bounds = {bottom:-1,top:-1}; + array.map(dom.childNodes, function(n){ + var b = n.getBoundingClientRect(); + if (b.bottom === 0) {return;} + if (bounds.top < 0 || b.top < bounds.top) { + bounds.top = b.top; + } + if (bounds.bottom < 0 || b.bottom > bounds.bottom) { + bounds.bottom = b.bottom; + } + }); + var height = bounds.bottom - bounds.top; + if (max && height > max) { + self.contentNode.parentNode.style['overflow-y'] = "auto"; + dom.style.height = max+"px"; + } else { + dom.style.height = height+"px"; + } + } + self.contentNode.parentNode.style.overflow = "hidden"; + heightFromChildrens(self.contentNode, self.contentMaxHeight); + heightFromChildrens(self.contentNode.parentNode); + heightFromChildrens(self.contentNode.parentNode.parentNode); + self.resize(); + self._willUpdateHeight = null; + }, 10); + } + } + }); +}); diff --git a/master/buildbot/www/static/js/lib/ui/dgridext/StyledColumns.js b/master/buildbot/www/static/js/lib/ui/dgridext/StyledColumns.js new file mode 100644 index 00000000000..23aff0e6251 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/dgridext/StyledColumns.js @@ -0,0 +1,15 @@ +define(["dojo/_base/declare"], +function(declare, array){ + /* ultra simple feature that adds style configurability to dgrid + for some reason, they decided to force this into css, while its often + much more practicle to decide it in the grid column declaration + */ + return declare(null, { + _configColumn: function(column, columnId, rowColumns, prefix){ + if (column.style) { + this.styleColumn(columnId, column.style); + } + return this.inherited(arguments); + } + }); +}); diff --git a/master/buildbot/www/static/js/lib/ui/dgridext/TypedColumns.js b/master/buildbot/www/static/js/lib/ui/dgridext/TypedColumns.js new file mode 100644 index 00000000000..185fb312581 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/dgridext/TypedColumns.js @@ -0,0 +1,73 @@ +define(["dojo/_base/declare", "dojo/_base/lang", "dojo/_base/array","moment"], +function(declare, lang, array, moment){ + /* implement basic types for columns to display them in a nice way + */ + return declare(null, { + _configColumn: function(column, columnId, rowColumns, prefix){ + var func = this["_configColumn_"+column.type]; + if (func !== undefined) { + func(column, columnId, rowColumns, prefix); + } + return this.inherited(arguments); + }, + _configColumn_bool: function(column, columnId, rowColumns, prefix){ + column.formatter = function(s) { + if (s) { + return "
Yes
"; + } + else { + return "
No
"; + } + }; + if (column.style === undefined){ + column.style="width:50px;"; + } + }, + _configColumn_date: function(column, columnId, rowColumns, prefix){ + column.formatter = function(s) { + if (s) { + var d = moment.unix(s); + return ""+d.fromNow()+"" ; + }else { + return "n/a"; + } + }; + if (column.style === undefined){ + column.style="width:110px;"; + } + }, + _configColumn_url: function(column, columnId, rowColumns, prefix){ + column.formatter = function(s) { + return ""+s; + }; + }, + _configColumn_revision: function(column, columnId, rowColumns, prefix){ + column.get = function(o) { return o;}; + column.formatter = function(o) { + return ""+o.revision; + }; + }, + _configColumn_filelist: function(column, columnId, rowColumns, prefix){ + column.formatter = function(f) { + var r = "
    "; + array.map(f, function(file) { + r += "
  • "+file+"
  • "; + }); + return r+"
      "; + }; + }, + _configColumn_user: function(column, columnId, rowColumns, prefix){ + column.formatter = function(s) { + if (s) { + var Name = lang.trim(s.split("<")[0]); + var id = Name; + if (s.split("<").length>1) { + id = lang.trim(s.split("<")[1].split(">")[0]); + } + return ""+Name; + } + return "unknown"; + }; + } + }); +}); diff --git a/master/buildbot/www/static/js/lib/ui/home.js b/master/buildbot/www/static/js/lib/ui/home.js index 4b3b7ac7a89..30a4c297f14 100644 --- a/master/buildbot/www/static/js/lib/ui/home.js +++ b/master/buildbot/www/static/js/lib/ui/home.js @@ -14,7 +14,7 @@ // Copyright Buildbot Team Members define(["dojo/_base/declare", "lib/ui/base", - "lib/haml!./templates/home.haml" + "./templates/home.haml" ], function(declare, Base, template) { "use strict"; return declare([Base], { diff --git a/master/buildbot/www/static/js/lib/ui/templates/Makefile b/master/buildbot/www/static/js/lib/ui/templates/Makefile new file mode 100644 index 00000000000..78fbba8c5f2 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/Makefile @@ -0,0 +1,3 @@ +all: + hamlcc *.haml + git add *.haml.js diff --git a/master/buildbot/www/static/js/lib/ui/templates/build.haml b/master/buildbot/www/static/js/lib/ui/templates/build.haml index fbdc6f5f896..25d88a642d1 100644 --- a/master/buildbot/www/static/js/lib/ui/templates/build.haml +++ b/master/buildbot/www/static/js/lib/ui/templates/build.haml @@ -1,3 +1,9 @@ +-#lint-input { +-# b:{builderName:"a",results:"OK",text:[],currentStep:{name:"a",text:[]}}, +-# number:1, +-# btn_class:function(){}, +-# isFinished:function(){} +-# } .container-fluid %ul.breadcrumb %li diff --git a/master/buildbot/www/static/js/lib/ui/templates/build.haml.js b/master/buildbot/www/static/js/lib/ui/templates/build.haml.js new file mode 100644 index 00000000000..6a50b4ca333 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/build.haml.js @@ -0,0 +1 @@ +define([],function(){function anonymous(locals){function _e(e){return(e+"").replace(/&/g,"&").replace(//g,">").replace(/\"/g,""")}with(locals||{})try{var _$output='
      Actions:'+function(){return isFinished()?' ':""}.call(this)+'
      '+function(){return isFinished()?"":'

      Build In Progress:

      '+function(){return b.when_time?"

      ETA: "+_e(b.when_time)+" [ "+_e(when)+" ]

      ":""}.call(this)+'
    • '+_e(b.currentStep.name)+", "+_e(b.currentStep.text.join(" "))+'  
      Stop Build
    • '}.call(this)+'

      Steps and Logfiles:

        '+function(){var e=[],t,n;for(t in b.steps)b.steps.hasOwnProperty(t)&&(n=b.steps[t],e.push('
      1. "+'
          '+function(){return n.logs.length===0?"
          - no logs -
          ":""}.call(this)+function(){var e=[],t,r;for(t in n.logs)n.logs.hasOwnProperty(t)&&(r=n.logs[t],e.push('
        1. '+""+_e(r[0])+"
        2. "));return e.join("")}.call(this)+function(){var e=[],t,r;for(t in n.urls)n.urls.hasOwnProperty(t)&&(r=n.urls[t],e.push('
        3. '+""+_e(r[0])+"
        4. "));return e.join("")}.call(this)+"
      2. "));return e.join("")}.call(this)+'

      build properties

      ';return _$output}catch(e){return"\n
      "+_e(e.stack)+"
      \n"}}return anonymous}) \ No newline at end of file diff --git a/master/buildbot/www/static/js/lib/ui/templates/builder.haml b/master/buildbot/www/static/js/lib/ui/templates/builder.haml index 2551cb22931..c760c243918 100644 --- a/master/buildbot/www/static/js/lib/ui/templates/builder.haml +++ b/master/buildbot/www/static/js/lib/ui/templates/builder.haml @@ -1,3 +1,4 @@ +-#lint-input {builder:{pendingBuilds:1},state:"building"} .container-fluid %ul.breadcrumb %li diff --git a/master/buildbot/www/static/js/lib/ui/templates/builder.haml.js b/master/buildbot/www/static/js/lib/ui/templates/builder.haml.js new file mode 100644 index 00000000000..78600e25f9c --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/builder.haml.js @@ -0,0 +1 @@ +define([],function(){function anonymous(locals){function _e(e){return(e+"").replace(/&/g,"&").replace(//g,">").replace(/\"/g,""")}with(locals||{})try{var _$output='
      ';return _$output}catch(e){return"\n
      "+_e(e.stack)+"
      \n"}}return anonymous}) \ No newline at end of file diff --git a/master/buildbot/www/static/js/lib/ui/templates/builders.haml.js b/master/buildbot/www/static/js/lib/ui/templates/builders.haml.js new file mode 100644 index 00000000000..8b912b09176 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/builders.haml.js @@ -0,0 +1 @@ +define([],function(){function anonymous(locals){function _e(e){return(e+"").replace(/&/g,"&").replace(//g,">").replace(/\"/g,""")}with(locals||{})try{var _$output='';return _$output}catch(e){return"\n
      "+_e(e.stack)+"
      \n"}}return anonymous}) \ No newline at end of file diff --git a/master/buildbot/www/static/js/lib/ui/templates/buildmasters.haml b/master/buildbot/www/static/js/lib/ui/templates/buildmasters.haml new file mode 100644 index 00000000000..01afa3d8217 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/buildmasters.haml @@ -0,0 +1,5 @@ +.container-fluid + %ul.breadcrumb + %li + %a{href:"#/buildmasters"}BuildMasters + .buildersgrid{data-dojo-attach-point:"buildmastersgrid_node"} diff --git a/master/buildbot/www/static/js/lib/ui/templates/buildmasters.haml.js b/master/buildbot/www/static/js/lib/ui/templates/buildmasters.haml.js new file mode 100644 index 00000000000..83943814598 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/buildmasters.haml.js @@ -0,0 +1 @@ +define([],function(){function anonymous(locals){function _e(e){return(e+"").replace(/&/g,"&").replace(//g,">").replace(/\"/g,""")}with(locals||{})try{var _$output='';return _$output}catch(e){return"\n
      "+_e(e.stack)+"
      \n"}}return anonymous}) \ No newline at end of file diff --git a/master/buildbot/www/static/js/lib/ui/templates/changes.haml b/master/buildbot/www/static/js/lib/ui/templates/changes.haml index 70298789d8c..d7dfab53701 100644 --- a/master/buildbot/www/static/js/lib/ui/templates/changes.haml +++ b/master/buildbot/www/static/js/lib/ui/templates/changes.haml @@ -10,8 +10,4 @@ .span12 %h2 Last changes coming into the system .row-fluid - .span5 .mainchangegrid{data-dojo-attach-point:"maingrid_node"} - .span7 - %h4 Associated Files - .mainchangegrid{data-dojo-attach-point:"filegrid_node"} diff --git a/master/buildbot/www/static/js/lib/ui/templates/changes.haml.js b/master/buildbot/www/static/js/lib/ui/templates/changes.haml.js new file mode 100644 index 00000000000..4fd6f82e74a --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/changes.haml.js @@ -0,0 +1 @@ +define([],function(){function anonymous(locals){function _e(e){return(e+"").replace(/&/g,"&").replace(//g,">").replace(/\"/g,""")}with(locals||{})try{var _$output='

      Last changes coming into the system

      ';return _$output}catch(e){return"\n
      "+_e(e.stack)+"
      \n"}}return anonymous}) \ No newline at end of file diff --git a/master/buildbot/www/static/js/lib/ui/templates/error.haml b/master/buildbot/www/static/js/lib/ui/templates/error.haml index 128b5ba7acd..0428e5d3887 100644 --- a/master/buildbot/www/static/js/lib/ui/templates/error.haml +++ b/master/buildbot/www/static/js/lib/ui/templates/error.haml @@ -1,3 +1,4 @@ +-#lint-input {error_msg:""} .container-fluid .row-fluid .span4 diff --git a/master/buildbot/www/static/js/lib/ui/templates/error.haml.js b/master/buildbot/www/static/js/lib/ui/templates/error.haml.js new file mode 100644 index 00000000000..73b0077abaa --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/error.haml.js @@ -0,0 +1 @@ +define([],function(){function anonymous(locals){function _e(e){return(e+"").replace(/&/g,"&").replace(//g,">").replace(/\"/g,""")}with(locals||{})try{var _$output='
      Error: '+error_msg+"
      ";return _$output}catch(e){return"\n
      "+_e(e.stack)+"
      \n"}}return anonymous}) \ No newline at end of file diff --git a/master/buildbot/www/static/js/lib/ui/templates/home.haml b/master/buildbot/www/static/js/lib/ui/templates/home.haml index 4d2ab9ed30e..e6de16e933e 100644 --- a/master/buildbot/www/static/js/lib/ui/templates/home.haml +++ b/master/buildbot/www/static/js/lib/ui/templates/home.haml @@ -1,3 +1,4 @@ +-#lint-input {recent_builds:[{title:"a",url:"b"}],recent_builders:[{title:"a",url:"b"}]} .container-fluid %ul.breadcrumb %li diff --git a/master/buildbot/www/static/js/lib/ui/templates/home.haml.js b/master/buildbot/www/static/js/lib/ui/templates/home.haml.js new file mode 100644 index 00000000000..3f3fb52da04 --- /dev/null +++ b/master/buildbot/www/static/js/lib/ui/templates/home.haml.js @@ -0,0 +1 @@ +define([],function(){function anonymous(locals){function _e(e){return(e+"").replace(/&/g,"&").replace(//g,">").replace(/\"/g,""")}with(locals||{})try{var _$output='

      Welcome to buildbot

      Probably we should display information about the current config
      • Buildbot version
      • Twisted version
      • Project Name
      • Project Version
      • Project Url
      ';return _$output}catch(e){return"\n
      "+_e(e.stack)+"
      \n"}}return anonymous}) \ No newline at end of file diff --git a/master/buildbot/www/ui.html b/master/buildbot/www/ui.html index f7470a6d654..edb43a0454f 100644 --- a/master/buildbot/www/ui.html +++ b/master/buildbot/www/ui.html @@ -90,17 +90,15 @@ The router global object will take care of dynamically load the correct content widgets and fill out the navbar --> - + diff --git a/master/buildbot/www/ui.py b/master/buildbot/www/ui.py index a81b08c4842..edf21f28d4b 100644 --- a/master/buildbot/www/ui.py +++ b/master/buildbot/www/ui.py @@ -24,55 +24,21 @@ class UIResource(resource.Resource): isLeaf = True - def __init__(self, master): + def __init__(self, master, extra_routes): """ Config can pass a static_url for serving static stuff directly from apache or nginx """ + self.extra_routes = extra_routes resource.Resource.__init__(self, master) - + def render(self, request): contents = dict( base_url = self.base_url, static_url = self.static_url, - ws_url = self.base_url.replace("http:", "ws:")) + ws_url = self.base_url.replace("http:", "ws:"), + extra_routes = self.extra_routes) + # IE8 ignore doctype html in corporate intranets + # this additionnal header removes this behavior and put + # IE8 in compatibility mode + request.setHeader("X-UA-Compatible" ,"IE=edge") return html % contents - -if __name__ == '__main__': - from twisted.application import strports, service - from twisted.web import server, static - from twisted.internet import reactor - class myStaticFile(static.File): - """Fix issue in twisted static implementation - where a 304 Not Modified always returns text/html - which makes chrome complain a lot in its logs""" - def render_GET(self, request): - r = static.File.render_GET(self, request) - if r=="": - request.setHeader('content-type', self.type) - return r - - class WWWService(service.MultiService): - def __init__(self): - service.MultiService.__init__(self) - class fakeConfig(): - www = dict(url="http://localhost:8010/", port=8010) - class fakeMaster(): - config = fakeConfig() - self.master = fakeMaster() - self.setup_site() - self.port_service = strports.service("8010", self.site) - self.port_service.setServiceParent(self) - self.startService() - def setup_site(self): - root = static.Data('placeholder', 'text/plain') - # redirect the root to UI - root.putChild('', resource.RedirectResource(self.master, 'ui/')) - # /ui - root.putChild('ui', UIResource(self.master)) - # /static - staticdir = util.sibpath(__file__, 'static') - - root.putChild('static', myStaticFile(staticdir)) - self.site = server.Site(root) - WWWService() - reactor.run() diff --git a/master/docs/developer/www.rst b/master/docs/developer/www.rst index 13777a4988f..d0c4c5e3cb5 100644 --- a/master/docs/developer/www.rst +++ b/master/docs/developer/www.rst @@ -20,15 +20,11 @@ To enable it, simply add following code in your ``master.cfg``: c['www'] = dict(port=8010) -Unpack the Dojo SDK from the `download page `_ into ``master/buildbot/www/static/js/external``: +Then run this command inside master.cfg directory:: -.. code-block:: sh + buildbot updatejs -d - mkdir -p master/buildbot/www/static/js/external - curl http://download.dojotoolkit.org/release-1.7.3/dojo-release-1.7.3-src.tar.gz | \ - tar --strip-components 1 -C master/buildbot/www/static/js/external -zxf - - -You'll also need ``dojo/dijit/dgrid/put-selector``. +This will automatically download js dependancies, and install the JS framework in developer mode Server Side Design ~~~~~~~~~~~~~~~~~~~ @@ -39,6 +35,8 @@ The server resource set is divided into 4 main paths: This html page is a simple skeleton, and a placeholder that is then filled out by JS * /static - static files. The JavaScript framework is actually capable of getting these static files from another path, allowing high-volume installations to serve those files directly via Nginx or Apache. + Those files are anyway served in public_html path, and are copied or symlinked by the create-master/upgrade-master/updatejs scripts. + This path is itself divided into several paths: * /static/css - The source for CSS files @@ -46,7 +44,6 @@ The server resource set is divided into 4 main paths: * /static/js/lib - The javascript source code of all buildbot js framework * /static/js/lib/ui - The ui javascript files * /static/js/lib/ui/templates - The html/haml templates for the ui - * /static/js/external - path for external js libs that needs to be downloaded separatly from buildbot * /api - This is the root of the REST interface. Paths below here can be used either for reading status or for controlling Buildbot (start builds, stop builds, reload config, etc). @@ -122,18 +119,15 @@ Haml is a templating engine originally made for ruby on rails, and later ported The language used for Buildbot, differs in the fact that JavaScript syntax is used instead of Ruby for evaluated expressions. An excellent tutorial is provided in the `haml-js website `_ -The version that buildbot uses is slighlty modified, in order to fit Dojo's AMD module definition, and to add some syntactic sugar to import Haml files. -The Haml files can be loaded using a Dojo plugin, similar to ``dojo/text!``: +We use `hamlcc website `_ to actually use the haml templates. +This tool compiles an haml file into a js function that can be easily embedded into a dojo build +in order to avoid having buildbot depend on haml and hamlcc, we store the js version of the files +Once you have installed node.js, and npm, using your distro packages, it is very easily installed +with npm:: -.. code-block:: js + sudo npm install -g hamlcc - define(["dojo/_base/declare", "lib/ui/base", - "lib/haml!./templates/build.haml" - ], function(declare, Base, template) { - "use strict"; - return declare([Base], { - templateFunc : template, - ... +hamlcc also include a very useful hamlint tool in order to pre-check its code inside the editor haml emacs mode is `available `_ diff --git a/master/docs/manual/cfg-global.rst b/master/docs/manual/cfg-global.rst index 1d43efc7a12..5230f62f4d7 100644 --- a/master/docs/manual/cfg-global.rst +++ b/master/docs/manual/cfg-global.rst @@ -666,11 +666,21 @@ following keys: This is probably not what you want! ``public_html`` - An optional root directory for files that will be served by the web server. + The root directory for files that will be served by the web server. Note that the ``public_html`` directory will only be searched for URLs that do not match Buildbot's built-in resources -- in particular, the - ``static/``, ``ui/``, and ``api/`` paths are reserved, although more + ``ui/``, ``ws``, and ``api/`` paths are reserved, although more reserved paths may be added in future versions. + Please note that ``static/`` directory within ``public_html`` is used to contain + static files of the web ui, including JS. The JS part will be synchronized at each + buildbot upgrade-master. Thus this needs to be writeable at upgrade-master time. + There is also a developer mode that setups symlinks to the source code instead of copying the files. + +``extra_js`` + List of extra JS AMD style packages to include in the static/js directory. + Each AMD packages is searched for a routes.js file containing additionnal routes, for + adding extra features to the UI. + See developer documentation for more info. ``static_url`` If present, this key gives the URL corresponding to :bb:src:`master/buildbot/www/static`. diff --git a/master/docs/manual/cmdline.rst b/master/docs/manual/cmdline.rst index fc2fd79b72f..1dec1f82c05 100644 --- a/master/docs/manual/cmdline.rst +++ b/master/docs/manual/cmdline.rst @@ -75,6 +75,33 @@ This creates a new directory and populates it with files that allow it to be use You will usually want to use the :option:`-r` option to create a relocatable :file:`buildbot.tac`. This allows you to move the master directory without editing this file. +.. bb:cmdline:: upgrade-master + +upgrade-master +++++++++++++++ + +.. code-block:: none + + buildbot upgrade-master {BASEDIR} + +This upgrades a previously created buildmaster's base directory for a new version of buildbot master source code. +This will copy the web server static files, and potencially upgrade the db. + +.. bb:cmdline:: updatejs + +updatejs +++++++++ + +.. code-block:: none + + buildbot updatejs [-d] {BASEDIR} + +This update the javascript files for a new buildbot master source code. It performs downloading of the js dependancies, and +minification of the js source code. + +Note: in order to have minification of customized JS code working, you need java and node installed. +The release tarballs of buildbot > 0.9 already contains minified version of JS code. + .. bb:cmdline:: start (buildbot) start diff --git a/master/setup.py b/master/setup.py index 42f66edb72e..42ae7dbce43 100755 --- a/master/setup.py +++ b/master/setup.py @@ -37,6 +37,12 @@ def include(d, e): return (d, [f for f in glob.glob('%s/%s'%(d, e)) if os.path.isfile(f)]) +def include_statics(d): + r = [] + for root, ds, fs in os.walk(d): + r.append((root, [ os.path.join(root, f) for f in fs])) + return r + class install_data_twisted(install_data): """make sure data files are installed in package. this is evil. @@ -119,6 +125,7 @@ def make_release_tree(self, base_dir, files): 'packages': ["buildbot", "buildbot.status", "buildbot.status.web","buildbot.status.web.hooks", + "buildbot.www", "buildbot.changes", "buildbot.steps", "buildbot.steps.package", @@ -164,7 +171,7 @@ def make_release_tree(self, base_dir, files): "buildbot/scripts/sample.cfg", "buildbot/scripts/buildbot_tac.tmpl", ]), - ], + ] + include_statics("buildbot/www/static"), 'scripts': scripts, 'cmdclass': {'install_data': install_data_twisted, 'sdist': our_sdist},