From e227d7e52621242cea2b3dae9ed98c972e5b64ec Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 26 Feb 2015 17:45:28 -0500 Subject: [PATCH 1/4] Start on the new build view --- endpoints/api/build.py | 18 ++- endpoints/api/trigger.py | 3 +- endpoints/common.py | 3 +- endpoints/trigger.py | 32 +++- initdb.py | 5 + static/css/core-ui.css | 22 ++- static/css/directives/ui/build-info-bar.css | 34 +++++ static/css/directives/ui/build-logs-view.css | 128 ++++++++++++++++ .../css/directives/ui/source-commit-link.css | 4 + static/css/directives/ui/source-ref-link.css | 4 + .../ui/triggered-build-description.css | 37 +++++ static/css/pages/build-view.css | 46 ++++++ static/css/quay.css | 2 +- static/directives/build-info-bar.html | 18 +++ static/directives/build-logs-view.html | 30 ++++ static/directives/logs-view.html | 1 - static/directives/resource-view.html | 6 +- static/directives/source-commit-link.html | 4 + static/directives/source-ref-link.html | 15 ++ .../triggered-build-description.html | 50 +++++++ static/js/app.js | 3 + static/js/directives/ui/build-info-bar.js | 19 +++ static/js/directives/ui/build-logs-view.js | 139 ++++++++++++++++++ static/js/directives/ui/resource-view.js | 20 +++ static/js/directives/ui/source-commit-link.js | 22 +++ static/js/directives/ui/source-ref-link.js | 41 ++++++ .../js/directives/ui/trigger-description.js | 2 +- .../ui/triggered-build-description.js | 21 +++ static/js/pages/build-view.js | 51 +++++++ static/partials/build-view.html | 47 ++++++ 30 files changed, 816 insertions(+), 11 deletions(-) create mode 100644 static/css/directives/ui/build-info-bar.css create mode 100644 static/css/directives/ui/build-logs-view.css create mode 100644 static/css/directives/ui/source-commit-link.css create mode 100644 static/css/directives/ui/source-ref-link.css create mode 100644 static/css/directives/ui/triggered-build-description.css create mode 100644 static/css/pages/build-view.css create mode 100644 static/directives/build-info-bar.html create mode 100644 static/directives/build-logs-view.html create mode 100644 static/directives/source-commit-link.html create mode 100644 static/directives/source-ref-link.html create mode 100644 static/directives/triggered-build-description.html create mode 100644 static/js/directives/ui/build-info-bar.js create mode 100644 static/js/directives/ui/build-logs-view.js create mode 100644 static/js/directives/ui/source-commit-link.js create mode 100644 static/js/directives/ui/source-ref-link.js create mode 100644 static/js/directives/ui/triggered-build-description.js create mode 100644 static/js/pages/build-view.js create mode 100644 static/partials/build-view.html diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 476c9ef72..9916e1021 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -93,7 +93,11 @@ def build_status_view(build_obj, can_write=False): 'is_writer': can_write, 'trigger': trigger_view(build_obj.trigger), 'resource_key': build_obj.resource_key, - 'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None + 'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None, + 'repository': { + 'namespace': build_obj.repository.namespace_user.username, + 'name': build_obj.repository.name + } } if can_write: @@ -214,6 +218,18 @@ class RepositoryBuildList(RepositoryParamResource): @path_param('build_uuid', 'The UUID of the build') class RepositoryBuildResource(RepositoryParamResource): """ Resource for dealing with repository builds. """ + @require_repo_read + @nickname('getRepoBuild') + def get(self, namespace, repository, build_uuid): + """ Returns information about a build. """ + try: + build = model.get_repository_build(build_uuid) + except model.InvalidRepositoryBuildException: + raise NotFound() + + can_write = ModifyRepositoryPermission(namespace, repository).can() + return build_status_view(build, can_write) + @require_repo_admin @nickname('cancelRepoBuild') def delete(self, namespace, repository, build_uuid): diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 1c59be1ee..4788a6e65 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -421,7 +421,8 @@ class ActivateBuildTrigger(RepositoryParamResource): pull_robot_name = model.get_pull_robot_name(trigger) build_request = start_build(repo, dockerfile_id, tags, name, subdir, True, - pull_robot_name=pull_robot_name, trigger_metadata=metadata) + trigger=trigger, pull_robot_name=pull_robot_name, + trigger_metadata=metadata) except TriggerStartException as tse: raise InvalidRequest(tse.message) diff --git a/endpoints/common.py b/endpoints/common.py index 50c6239c8..f76a822e4 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -224,7 +224,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, 'docker_tags': tags, 'registry': host, 'build_subdir': subdir, - 'trigger_metadata': trigger_metadata or {} + 'trigger_metadata': trigger_metadata or {}, + 'is_manual': manual } with app.config['DB_TRANSACTION_FACTORY'](db): diff --git a/endpoints/trigger.py b/endpoints/trigger.py index cc248f798..bfd9f8ce7 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -310,6 +310,30 @@ class GithubBuildTrigger(BuildTrigger): message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source) raise RepositoryReadException(message) + @staticmethod + def _build_commit_info(repo, commit_sha): + try: + commit = repo.get_commit(commit_sha) + except GithubException: + logger.exception('Could not load data for commit') + return + + return { + 'url': commit.html_url, + 'message': commit.commit.message, + 'author': { + 'username': commit.author.login, + 'avatar_url': commit.author.avatar_url, + 'url': commit.author.html_url + }, + 'committer': { + 'username': commit.committer.login, + 'avatar_url': commit.committer.avatar_url, + 'url': commit.committer.html_url + }, + 'date': commit.last_modified + } + @staticmethod def _prepare_build(config, repo, commit_sha, build_name, ref): # Prepare the download and upload URLs @@ -360,9 +384,14 @@ class GithubBuildTrigger(BuildTrigger): metadata = { 'commit_sha': commit_sha, 'ref': ref, - 'default_branch': repo.default_branch + 'default_branch': repo.default_branch, } + # add the commit info. + commit_info = GithubBuildTrigger._build_commit_info(repo, commit_sha) + if commit_info is not None: + metadata['commit_info'] = commit_info + return dockerfile_id, list(tags), build_name, joined_subdir, metadata @staticmethod @@ -417,6 +446,7 @@ class GithubBuildTrigger(BuildTrigger): branch_name = run_parameters.get('branch_name') or repo.default_branch branch = repo.get_branch(branch_name) branch_sha = branch.commit.sha + commit_info = branch.commit short_sha = GithubBuildTrigger.get_display_name(branch_sha) ref = 'refs/heads/%s' % (branch_name) diff --git a/initdb.py b/initdb.py index 14fcc1441..9571e4fd1 100644 --- a/initdb.py +++ b/initdb.py @@ -411,6 +411,11 @@ def populate_database(): 'repository': repo, 'docker_tags': ['latest'], 'build_subdir': '', + 'trigger_metadata': { + 'commit_sha': '3482adc5822c498e8f7db2e361e8d57b3d77ddd9', + 'ref': 'refs/heads/master', + 'default_branch': 'master' + } } record = model.create_email_authorization_for_repo(new_user_1.username, 'simple', diff --git a/static/css/core-ui.css b/static/css/core-ui.css index 2012129c1..f1e5f4d28 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -717,4 +717,24 @@ .cor-container { padding-left: 15px; padding-right: 15px; -} \ No newline at end of file +} + +.cor-title-link { + line-height: 30px; + margin-top: 22px; + margin-bottom: 10px; + font-size: 16px; +} + +.cor-title-link a { + color: lightblue; +} + +.cor-title-link a.back-link:before { + content: "\f060"; + color: white; + display: inline-block; + margin-right: 10px; + vertical-align: middle; + font-family: FontAwesome; +} diff --git a/static/css/directives/ui/build-info-bar.css b/static/css/directives/ui/build-info-bar.css new file mode 100644 index 000000000..2421b89a9 --- /dev/null +++ b/static/css/directives/ui/build-info-bar.css @@ -0,0 +1,34 @@ +.build-info-bar-element { + position: relative; + padding: 6px; + padding-left: 20px; +} + +.build-info-bar-element .phase-icon { + position: absolute; + top: 4px; + left: 4px; + bottom: 0px; + height: auto; + border-radius: 0px; + width: 5px; +} + +.build-info-bar-element .build-side-info { + float: right; +} + +.build-info-bar-element .build-side-info .build-side-id { + font-size: 12px; + color: #ccc; + margin-bottom: 10px; +} + +.build-info-bar-element .build-side-info .timing .fa { + display: inline-block; + margin-right: 4px; +} + +.build-info-bar-element .build-side-info .timing { + text-align: right; +} \ No newline at end of file diff --git a/static/css/directives/ui/build-logs-view.css b/static/css/directives/ui/build-logs-view.css new file mode 100644 index 000000000..368caa772 --- /dev/null +++ b/static/css/directives/ui/build-logs-view.css @@ -0,0 +1,128 @@ +.build-logs-view { + position: relative; + color: white; + padding: 10px; + overflow: auto; + box-shadow: inset 10px 10px 5px -9px rgba(0,0,0,0.75); + background: linear-gradient(30deg, #263945, #061C2A) no-repeat left top fixed; +} + +.build-logs-view .container-header { + padding: 2px; +} + +.build-logs-view .container-logs { + margin: 4px; + padding-bottom: 4px; +} + +.build-logs-view .command-title, +.build-logs-view .log-entry .message, +.build-logs-view .log-entry .message span { + font-family: Consolas, "Lucida Console", Monaco, monospace; + font-size: 13px; +} + +.build-logs-view .container-header { + cursor: pointer; + position: relative; +} + +.build-logs-view .container-header i.fa.chevron { + color: #666; + margin-right: 4px; + width: 14px; + text-align: center; + + position: absolute; + top: 6px; + left: 0px; +} + +.build-logs-view .log-container.command { + margin-left: 22px; +} + +.build-logs-view .container-header.building { + margin-bottom: 10px; +} + +.build-logs-view .container-header.pushing { + margin-top: 10px; +} + +.build-logs-view .build-log-error-element .error-message-container { + position: relative; + display: inline-block; + margin: 10px; + padding: 10px; + background: rgba(255, 0, 0, 0.17); + border-radius: 10px; + margin-left: 22px; +} + +.build-logs-view .build-log-error-element .error-message-container i.fa { + color: red; + position: absolute; + top: 13px; + left: 11px; +} + +.build-logs-view .build-log-error-element .error-message { + display: inline-block; + margin-left: 25px; +} + +.build-logs-view .container-header .label { + padding-top: 4px; + text-align: right; + margin-right: 4px; + width: 86px; + display: inline-block; + + border-right: 4px solid #aaa; + background-color: #717171; + + position: absolute; + top: 4px; + left: 24px; +} + +.build-logs-view .dockerfile-command { + position: inherit; +} + +.build-logs-view .dockerfile-command .command-title { + padding-left: 0px; +} + +.build-logs-view .container-header .container-content { + display: block; + padding-left: 20px; +} + +.build-logs-view .container-header .container-content.build-log-command { + padding-left: 120px; +} + +.build-logs-view .log-entry { + position: relative; +} + +.build-logs-view .log-entry .message { + display: inline-block; + margin-left: 46px; +} + +.build-logs-view .log-entry .id { + color: #aaa; + padding-right: 6px; + margin-right: 6px; + text-align: right; + font-size: 12px; + width: 40px; + + position: absolute; + top: 4px; + left: 4px; +} diff --git a/static/css/directives/ui/source-commit-link.css b/static/css/directives/ui/source-commit-link.css new file mode 100644 index 000000000..55966d164 --- /dev/null +++ b/static/css/directives/ui/source-commit-link.css @@ -0,0 +1,4 @@ +.source-commit-link-element .fa { + margin-right: 4px; + display: inline-block; +} \ No newline at end of file diff --git a/static/css/directives/ui/source-ref-link.css b/static/css/directives/ui/source-ref-link.css new file mode 100644 index 000000000..cf593ec07 --- /dev/null +++ b/static/css/directives/ui/source-ref-link.css @@ -0,0 +1,4 @@ +.source-ref-link-element .fa { + margin-right: 4px; + display: inline-block; +} \ No newline at end of file diff --git a/static/css/directives/ui/triggered-build-description.css b/static/css/directives/ui/triggered-build-description.css new file mode 100644 index 000000000..d881f8bb4 --- /dev/null +++ b/static/css/directives/ui/triggered-build-description.css @@ -0,0 +1,37 @@ +.triggered-build-description-element .commit-message { + font-size: 16px; + display: block; +} + +.triggered-build-description-element .commit-who img { + width: 16px; + height: 16px; +} + +.triggered-build-description-element .fa-github { + display: inline-block; + margin-left: 6px; + margin-right: 4px; +} + +.triggered-build-description-element .commit-information { + margin-top: 6px; + font-size: 14px; + color: #999; +} + +.triggered-build-description-element .source-commit-link a { + color: #999; +} + +.triggered-build-description-element .source-ref-link a { + color: #999; +} + +.triggered-build-description-element .commit-information > span { + margin-right: 20px; +} + +.triggered-build-description-element .commit-who:before { + content: "by "; +} \ No newline at end of file diff --git a/static/css/pages/build-view.css b/static/css/pages/build-view.css new file mode 100644 index 000000000..09d976b09 --- /dev/null +++ b/static/css/pages/build-view.css @@ -0,0 +1,46 @@ +.build-view .build-header .repo-breadcrumb { + font-size: 18px; + margin-left: 12px; +} + +.build-view .build-status-header { + padding: 4px; +} + +.build-view .build-status-header .cor-loader-inline { + margin-right: 10px; +} + +.build-view .build-logs-view { + margin-top: 10px; +} + +.build-view .build-status-header { + font-size: 17px; + margin-top: 10px; +} + +.build-view .build-status-header .fa { + margin-right: 6px; +} + +.build-view .build-icon-message.error { + color: red; +} + +.build-view .build-icon-message.internalerror { + color: #DFFF00; +} + +.build-view .build-icon-message.complete { + color: #2fcc66; +} + +.build-view .build-status-header .timing { + float: right; +} + +.build-view .build-status-header .timing .fa { + margin-left: 10px; + margin-right: 4px; +} diff --git a/static/css/quay.css b/static/css/quay.css index 4fc72cc49..bac39dcf2 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -866,7 +866,7 @@ i.toggle-icon:hover { } .phase-icon.complete { - background-color: rgba(66, 139, 202, 1); + background-color: #2fcc66; } .build-status { diff --git a/static/directives/build-info-bar.html b/static/directives/build-info-bar.html new file mode 100644 index 000000000..4a970d727 --- /dev/null +++ b/static/directives/build-info-bar.html @@ -0,0 +1,18 @@ +
+ + + + +
+ +
{{ build.id }}
+ + +
+ +
+
+ +
+
Manually Started Build
+
\ No newline at end of file diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html new file mode 100644 index 000000000..01091f16e --- /dev/null +++ b/static/directives/build-logs-view.html @@ -0,0 +1,30 @@ +
+ + + (No Logs) + +
+
+ +
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+
+
\ No newline at end of file diff --git a/static/directives/logs-view.html b/static/directives/logs-view.html index 28fc8edb0..651f91b3f 100644 --- a/static/directives/logs-view.html +++ b/static/directives/logs-view.html @@ -1,5 +1,4 @@
-
Usage Logs diff --git a/static/directives/resource-view.html b/static/directives/resource-view.html index 0952bf2f4..30e23559b 100644 --- a/static/directives/resource-view.html +++ b/static/directives/resource-view.html @@ -1,9 +1,9 @@
-
-
+
+
{{ errorMessage }}
-
+
diff --git a/static/directives/source-commit-link.html b/static/directives/source-commit-link.html new file mode 100644 index 000000000..53d645400 --- /dev/null +++ b/static/directives/source-commit-link.html @@ -0,0 +1,4 @@ + + + {{ commitSha.substring(0, 8) }} + \ No newline at end of file diff --git a/static/directives/source-ref-link.html b/static/directives/source-ref-link.html new file mode 100644 index 000000000..f02c7b7f7 --- /dev/null +++ b/static/directives/source-ref-link.html @@ -0,0 +1,15 @@ + + + + + + {{ getTitle(ref) }} + + + + + + {{ getTitle(ref) }} + + + \ No newline at end of file diff --git a/static/directives/triggered-build-description.html b/static/directives/triggered-build-description.html new file mode 100644 index 000000000..2c3ada7fc --- /dev/null +++ b/static/directives/triggered-build-description.html @@ -0,0 +1,50 @@ +
+ + + + + +
{{ build.job_config.trigger_metadata.commit_info.message }}
+ +
+ + + + Triggered by commit + + + + + + Triggered by commit to + + + {{ build.trigger.config.build_source }} + + +
+ + + +
+
\ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 12adc7521..48a9e1350 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -96,6 +96,9 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP // Repo Builds .route('/repository/:namespace/:name/build', 'repo-build') + // Repo Build View + .route('/repository/:namespace/:name/build/:buildid', 'build-view') + // Repo Build Package .route('/repository/:namespace/:name/build/:buildid/buildpack', 'build-package') diff --git a/static/js/directives/ui/build-info-bar.js b/static/js/directives/ui/build-info-bar.js new file mode 100644 index 000000000..32850ef6f --- /dev/null +++ b/static/js/directives/ui/build-info-bar.js @@ -0,0 +1,19 @@ +/** + * An element which displays the status of a build in a nice compact bar. + */ +angular.module('quay').directive('buildInfoBar', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/build-info-bar.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'build': '=build', + 'showTime': '=showTime' + }, + controller: function($scope, $element) { + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/directives/ui/build-logs-view.js b/static/js/directives/ui/build-logs-view.js new file mode 100644 index 000000000..4e186fd94 --- /dev/null +++ b/static/js/directives/ui/build-logs-view.js @@ -0,0 +1,139 @@ +/** + * An element which displays and auto-updates the logs from a build. + */ +angular.module('quay').directive('buildLogsView', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/build-logs-view.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'build': '=build', + 'buildUpdated': '&buildUpdated' + }, + controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray, + AngularPollChannel, ApiService, Restangular) { + + $scope.logEntries = null; + $scope.currentParentEntry = null; + $scope.logStartIndex = 0; + + var pollChannel = null; + var currentBuild = null; + + var processLogs = function(logs, startIndex, endIndex) { + if (!$scope.logEntries) { $scope.logEntries = []; } + + // If the start index given is less than that requested, then we've received a larger + // pool of logs, and we need to only consider the new ones. + if (startIndex < $scope.logStartIndex) { + logs = logs.slice($scope.logStartIndex - startIndex); + } + + for (var i = 0; i < logs.length; ++i) { + var entry = logs[i]; + var type = entry['type'] || 'entry'; + if (type == 'command' || type == 'phase' || type == 'error') { + entry['logs'] = AngularViewArray.create(); + entry['index'] = $scope.logStartIndex + i; + + $scope.logEntries.push(entry); + $scope.currentParentEntry = entry; + } else if ($scope.currentParentEntry) { + $scope.currentParentEntry['logs'].push(entry); + } + } + + return endIndex; + }; + + var getBuildStatusAndLogs = function(build, callback) { + var params = { + 'repository': build.repository.namespace + '/' + build.repository.name, + 'build_uuid': build.id + }; + + ApiService.getRepoBuildStatus(null, params, true).then(function(resp) { + if (resp.id != $scope.build.id) { callback(false); return; } + + // Call the build updated handler. + $scope.buildUpdated({'build': resp}); + + // Save the current build. + currentBuild = resp; + + // Load the updated logs for the build. + var options = { + 'start': $scope.logStartIndex + }; + + ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) { + // Process the logs we've received. + $scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']); + + // If the build status is an error, open the last two log entries. + if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) { + var openLogEntries = function(entry) { + if (entry.logs) { + entry.logs.setVisible(true); + } + }; + + openLogEntries($scope.logEntries[$scope.logEntries.length - 2]); + openLogEntries($scope.logEntries[$scope.logEntries.length - 1]); + } + + // If the build phase is an error or a complete, then we mark the channel + // as closed. + callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete'); + }, function() { + callback(false); + }); + }, function() { + callback(false); + }); + }; + + var startWatching = function(build) { + // Create a new channel for polling the build status and logs. + var conductStatusAndLogRequest = function(callback) { + getBuildStatusAndLogs(build, callback); + }; + + pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */); + pollChannel.start(); + }; + + var stopWatching = function() { + if (pollChannel) { + pollChannel.stop(); + pollChannel = null; + } + }; + + $scope.$watch('build', function(build) { + if (build) { + startWatching(build); + } else { + stopWatching(); + } + }); + + $scope.hasLogs = function(container) { + return container.logs.hasEntries; + }; + + $scope.processANSI = function(message, container) { + var filter = container.logs._filter = (container.logs._filter || ansi2html.create()); + + // Note: order is important here. + var setup = filter.getSetupHtml(); + var stream = filter.addInputToStream(message); + var teardown = filter.getTeardownHtml(); + return setup + stream + teardown; + }; + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/directives/ui/resource-view.js b/static/js/directives/ui/resource-view.js index a0e71c0e0..c7fc2e4c1 100644 --- a/static/js/directives/ui/resource-view.js +++ b/static/js/directives/ui/resource-view.js @@ -11,9 +11,29 @@ angular.module('quay').directive('resourceView', function () { restrict: 'C', scope: { 'resource': '=resource', + 'resources': '=resources', 'errorMessage': '=errorMessage' }, controller: function($scope, $element) { + $scope.getState = function() { + if (!$scope.resources && !$scope.resource) { + return 'loading'; + } + + var resources = $scope.resources || [$scope.resource]; + for (var i = 0; i < resources.length; ++i) { + var current = resources[i]; + if (current.loading) { + return 'loading'; + } + + if (current.error) { + return 'error'; + } + } + + return 'ready'; + }; } }; return directiveDefinitionObject; diff --git a/static/js/directives/ui/source-commit-link.js b/static/js/directives/ui/source-commit-link.js new file mode 100644 index 000000000..68bb42bd4 --- /dev/null +++ b/static/js/directives/ui/source-commit-link.js @@ -0,0 +1,22 @@ +/** + * An element which displays a link to a commit in Git or Mercurial or another source control. + */ +angular.module('quay').directive('sourceCommitLink', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/source-commit-link.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'commitSha': '=commitSha', + 'urlTemplate': '=urlTemplate' + }, + controller: function($scope, $element) { + $scope.getUrl = function(sha, template) { + return template.replace('{sha}', sha); + }; + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/directives/ui/source-ref-link.js b/static/js/directives/ui/source-ref-link.js new file mode 100644 index 000000000..170ff3f04 --- /dev/null +++ b/static/js/directives/ui/source-ref-link.js @@ -0,0 +1,41 @@ +/** + * An element which displays a link to a branch or tag in source control. + */ +angular.module('quay').directive('sourceRefLink', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/source-ref-link.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'ref': '=ref', + 'branchTemplate': '=branchTemplate', + 'tagTemplate': '=tagTemplate' + }, + controller: function($scope, $element) { + $scope.getKind = function(ref) { + var parts = (ref || '').split('/'); + if (parts.length < 3) { + return ''; + } + + return parts[1]; + }; + + $scope.getTitle = function(ref) { + var parts = (ref || '').split('/'); + if (parts.length < 3) { + return ''; + } + + return parts[2]; + }; + + $scope.getUrl = function(ref, template, kind) { + return template.replace('{' + kind + '}', $scope.getTitle(ref)); + }; + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/directives/ui/trigger-description.js b/static/js/directives/ui/trigger-description.js index 9b09960bc..0921fd023 100644 --- a/static/js/directives/ui/trigger-description.js +++ b/static/js/directives/ui/trigger-description.js @@ -1,5 +1,5 @@ /** - * An element which displays information about a build trigger. + * DEPRECATED: An element which displays information about a build trigger. */ angular.module('quay').directive('triggerDescription', function () { var directiveDefinitionObject = { diff --git a/static/js/directives/ui/triggered-build-description.js b/static/js/directives/ui/triggered-build-description.js new file mode 100644 index 000000000..63cafa779 --- /dev/null +++ b/static/js/directives/ui/triggered-build-description.js @@ -0,0 +1,21 @@ +/** + * An element which displays information about a build that was triggered from an outside source. + */ +angular.module('quay').directive('triggeredBuildDescription', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/triggered-build-description.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'build': '=build' + }, + controller: function($scope, $element, KeyService, TriggerService) { + $scope.getGitHubRepoURL = function(build) { + return KeyService['githubTriggerEndpoint'] + build.trigger.config.build_source + '/'; + }; + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/pages/build-view.js b/static/js/pages/build-view.js new file mode 100644 index 000000000..750caadf2 --- /dev/null +++ b/static/js/pages/build-view.js @@ -0,0 +1,51 @@ +(function() { + /** + * Build view page. Displays the view of a particular build for a repository. + */ + angular.module('quayPages').config(['pages', function(pages) { + pages.create('build-view', 'build-view.html', BuildViewCtrl, { + newLayout: true + }); + }]); + + function BuildViewCtrl($scope, ApiService, $routeParams, AngularPollChannel) { + $scope.namespace = $routeParams.namespace; + $scope.name = $routeParams.name; + $scope.build_uuid = $routeParams.buildid; + + var loadBuild = function() { + var params = { + 'repository': $scope.namespace + '/' + $scope.name, + 'build_uuid': $scope.build_uuid + }; + + $scope.buildResource = ApiService.getRepoBuildAsResource(params).get(function(build) { + $scope.build = build; + $scope.originalBuild = build; + }); + }; + + var loadRepository = function() { + var params = { + 'repository': $scope.namespace + '/' + $scope.name + }; + + $scope.repoResource = ApiService.getRepoAsResource(params).get(function(repo) { + $scope.repo = repo; + }, ApiService.errorDisplay('Cannot load repository')); + }; + + // Page startup: + loadRepository(); + loadBuild(); + + $scope.setUpdatedBuild = function(build) { + $scope.build = build; + }; + + $scope.isBuilding = function(build) { + if (!build) { return true; } + return build.phase != 'complete' && build.phase != 'error'; + }; + } +})(); \ No newline at end of file diff --git a/static/partials/build-view.html b/static/partials/build-view.html new file mode 100644 index 000000000..65d71af41 --- /dev/null +++ b/static/partials/build-view.html @@ -0,0 +1,47 @@ +
+
+ +
+
+ + + + {{ namespace }}/{{ name }} + + + + {{ build.display_name }} + +
+ +
+ +
+ + +
+ + + + + + + + + + +
+ + Build started + +
+
+ + +
+
+
+
+
\ No newline at end of file From ed46d37ea787a523c361abe0104b9500e09db03d Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 27 Feb 2015 16:00:32 -0500 Subject: [PATCH 2/4] - Add copy button to the build logs - Add support for timestamps in the build logs - Other small UI improvements to the build view --- static/css/directives/ui/build-logs-view.css | 57 ++++++++++++++++++- .../ui/triggered-build-description.css | 7 +++ static/css/pages/build-view.css | 5 ++ static/directives/build-logs-view.html | 15 +++-- static/directives/source-commit-link.html | 2 +- static/directives/source-ref-link.html | 4 +- .../triggered-build-description.html | 10 +++- static/js/directives/ui/build-logs-view.js | 29 ++++++++++ static/js/directives/ui/copy-box.js | 3 + static/js/pages/build-view.js | 24 +++++++- static/partials/build-view.html | 19 ++++++- test/testlogs.py | 32 +++++++++-- 12 files changed, 189 insertions(+), 18 deletions(-) diff --git a/static/css/directives/ui/build-logs-view.css b/static/css/directives/ui/build-logs-view.css index 368caa772..dca63e410 100644 --- a/static/css/directives/ui/build-logs-view.css +++ b/static/css/directives/ui/build-logs-view.css @@ -4,7 +4,13 @@ padding: 10px; overflow: auto; box-shadow: inset 10px 10px 5px -9px rgba(0,0,0,0.75); - background: linear-gradient(30deg, #263945, #061C2A) no-repeat left top fixed; + background-color: #263945; + + min-height: 100px; +} + +.build-logs-view .no-logs { + color: #8C8C8C; } .build-logs-view .container-header { @@ -109,6 +115,22 @@ position: relative; } +.build-logs-view .log-entry:hover { + background: rgba(0, 0, 0, 0.2); + cursor: pointer; +} + +.build-logs-view .log-entry:hover .timestamp { + visibility: visible; +} + +.build-logs-view .log-entry .timestamp { + float: right; + visibility: hidden; + color: #aaa; + margin-right: 10px; +} + .build-logs-view .log-entry .message { display: inline-block; margin-left: 46px; @@ -117,12 +139,41 @@ .build-logs-view .log-entry .id { color: #aaa; padding-right: 6px; - margin-right: 6px; + margin-right: 10px; text-align: right; font-size: 12px; width: 40px; position: absolute; - top: 4px; + top: 1px; left: 4px; } + +.build-logs-view .copy-button { + position: absolute; + top: 6px; + right: 6px; + z-index: 2; + transition: all 0.15s ease-in-out; +} + +.build-logs-view .copy-button:not(.zeroclipboard-is-hover) { + background: transparent; + border: 1px solid transparent; + color: #ddd; +} + +.build-logs-view .copy-button i.fa { + margin-right: 10px; +} + +.build-logs-view-element.with-timestamps .log-entry .message { + display: inline-block; + margin-left: 146px; +} + +.build-logs-view-element.with-timestamps .log-entry .id { + width: 140px; + white-space: nowrap; + padding-right: 10px; +} diff --git a/static/css/directives/ui/triggered-build-description.css b/static/css/directives/ui/triggered-build-description.css index d881f8bb4..2ef4914e3 100644 --- a/static/css/directives/ui/triggered-build-description.css +++ b/static/css/directives/ui/triggered-build-description.css @@ -3,9 +3,16 @@ display: block; } +.triggered-build-description-element .commit-message a { + color: black; +} + .triggered-build-description-element .commit-who img { width: 16px; height: 16px; + margin-left: 2px; + margin-right: 2px; + border-radius: 50%; } .triggered-build-description-element .fa-github { diff --git a/static/css/pages/build-view.css b/static/css/pages/build-view.css index 09d976b09..649f0005a 100644 --- a/static/css/pages/build-view.css +++ b/static/css/pages/build-view.css @@ -36,6 +36,11 @@ color: #2fcc66; } +.build-view .build-status-header .cor-options-menu { + float: right; + margin-left: 20px; +} + .build-view .build-status-header .timing { float: right; } diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html index 01091f16e..285a8b141 100644 --- a/static/directives/build-logs-view.html +++ b/static/directives/build-logs-view.html @@ -1,13 +1,18 @@ -
+
+ + - (No Logs) + (Waiting for build to start)
+ ng-class="container.logs.isVisible ? 'fa-chevron-down' : 'fa-chevron-right'" + ng-show="hasLogs(container)">
@@ -22,8 +27,10 @@
- + + +
diff --git a/static/directives/source-commit-link.html b/static/directives/source-commit-link.html index 53d645400..d8803b319 100644 --- a/static/directives/source-commit-link.html +++ b/static/directives/source-commit-link.html @@ -1,4 +1,4 @@ - + {{ commitSha.substring(0, 8) }} \ No newline at end of file diff --git a/static/directives/source-ref-link.html b/static/directives/source-ref-link.html index f02c7b7f7..dcf142001 100644 --- a/static/directives/source-ref-link.html +++ b/static/directives/source-ref-link.html @@ -2,13 +2,13 @@ - + {{ getTitle(ref) }} - + {{ getTitle(ref) }} diff --git a/static/directives/triggered-build-description.html b/static/directives/triggered-build-description.html index 2c3ada7fc..1aa33820f 100644 --- a/static/directives/triggered-build-description.html +++ b/static/directives/triggered-build-description.html @@ -4,9 +4,15 @@ -
{{ build.job_config.trigger_metadata.commit_info.message }}
+
+ Authored @@ -27,7 +33,7 @@ - + Triggered by commit + + + + Hide Timestamps + + + Show Timestamps + + + + Cancel Build + + +
Build started @@ -39,7 +54,9 @@
-
diff --git a/test/testlogs.py b/test/testlogs.py index 49dcf3c7c..cf140f085 100644 --- a/test/testlogs.py +++ b/test/testlogs.py @@ -1,4 +1,5 @@ import logging +import datetime from random import SystemRandom from functools import wraps, partial @@ -108,7 +109,13 @@ class TestBuildLogs(RedisBuildLogs): return script def _generate_phase(self, start_weight, phase_name): - return (start_weight, {'message': phase_name, 'type': self.PHASE}, + message = { + 'message': phase_name, + 'type': self.PHASE, + 'datetime': str(datetime.datetime.now()) + } + + return (start_weight, message, (phase_name, deepcopy(self.STATUS_TEMPLATE))) def _generate_command(self, command_num, total_commands, command_weight): @@ -123,6 +130,7 @@ class TestBuildLogs(RedisBuildLogs): msg = { 'message': 'Step %s: %s %s' % (command_num, command, sentence), 'type': self.COMMAND, + 'datetime': str(datetime.datetime.now()) } status = deepcopy(self.STATUS_TEMPLATE) status['total_commands'] = total_commands @@ -133,10 +141,26 @@ class TestBuildLogs(RedisBuildLogs): def _generate_logs(count): others = [] if random.randint(0, 10) <= 8: - count = count - 2 - others = [(1, {'message': '\x1b[91m' + get_sentence()}, None), (1, {'message': '\x1b[0m'}, None)] + premessage = { + 'message': '\x1b[91m' + get_sentence(), + 'datetime': str(datetime.datetime.now()) + } - return others + [(1, {'message': get_sentence()}, None) for _ in range(count)] + postmessage = { + 'message': '\x1b[0m', + 'datetime': str(datetime.datetime.now()) + } + + count = count - 2 + others = [(1, premessage, None), (1, postmessage, None)] + + def get_message(): + return { + 'message': get_sentence(), + 'datetime': str(datetime.datetime.now()) + } + + return others + [(1, get_message(), None) for _ in range(count)] @staticmethod def _compute_total_completion(statuses, total_images): From 7460541c89f0d99dcb63f7e2093ba719e1a9b191 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 27 Feb 2015 16:52:56 -0500 Subject: [PATCH 3/4] Work In Progress: Dynamic titles and descriptions --- static/js/app.js | 47 +++++++++++++----------- static/js/pages/build-view.js | 4 ++- static/js/services/meta-service.js | 58 ++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 static/js/services/meta-service.js diff --git a/static/js/app.js b/static/js/app.js index 48a9e1350..bc721d769 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -206,8 +206,8 @@ if (window.__config && window.__config.SENTRY_PUBLIC_DSN) { } // Run the application. -quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService', - function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService) { +quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService', 'MetaService', + function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService, MetaService) { var title = window.__config['REGISTRY_TITLE'] || 'Quay.io'; @@ -299,34 +299,39 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi }); $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { - $rootScope.pageClass = ''; $rootScope.current = current.$$route; + $rootScope.currentPage = current; + + $rootScope.pageClass = ''; if (!current.$$route) { return; } - if (current.$$route.title) { - $rootScope.title = current.$$route.title; - } else { - $rootScope.title = title; - } - - if (current.$$route.pageClass) { - $rootScope.pageClass = current.$$route.pageClass; - } - + $rootScope.pageClass = current.$$route.pageClass || ''; $rootScope.newLayout = !!current.$$route.newLayout; - - if (current.$$route.description) { - $rootScope.description = current.$$route.description; - } else { - $rootScope.description = ''; - } - $rootScope.fixFooter = !!current.$$route.fixFooter; + + MetaService.getInitialTitle(current, function(title) { + $rootScope.title = title; + }); + + MetaService.getInitialDescription(current, function(description) { + $rootScope.description = description + }); + $anchorScroll(); }); - $rootScope.$on('$viewContentLoaded', function(event, current) { + $rootScope.$on('$viewContentLoaded', function(event) { + var current = $rootScope.currentPage; + + MetaService.getTitle(current, function(title) { + $rootScope.title = title; + }); + + MetaService.getDescription(current, function(description) { + $rootScope.description = description; + }); + var activeTab = $location.search()['tab']; // Setup deep linking of tabs. This will change the search field of the URL whenever a tab diff --git a/static/js/pages/build-view.js b/static/js/pages/build-view.js index 908a25bd9..27fe4afd7 100644 --- a/static/js/pages/build-view.js +++ b/static/js/pages/build-view.js @@ -4,7 +4,9 @@ */ angular.module('quayPages').config(['pages', function(pages) { pages.create('build-view', 'build-view.html', BuildViewCtrl, { - newLayout: true + newLayout: true, + title: 'Build {{ build.display_name }}', + description: 'Logs and status for build {{ build.display_name }}' }); }]); diff --git a/static/js/services/meta-service.js b/static/js/services/meta-service.js new file mode 100644 index 000000000..0cd84fc89 --- /dev/null +++ b/static/js/services/meta-service.js @@ -0,0 +1,58 @@ +/** + * Service which helps set the contents of the tags (and the of a page). + */ +angular.module('quay').factory('MetaService', ['$interpolate', 'Config', '$rootScope', '$interval', + function($interpolate, Config, $rootScope, $interval) { + var metaService = {}; + + var interpolate = function(page, expr, callback) { + $rootScope.$watch(page.scope, function() { + var inter = $interpolate(expr, true, null, true); + callback(inter(page.scope)); + }); + }; + + var initial = function(value, default_value, callback) { + if (!value) { + callback(default_value); + return; + } + + if (value.indexOf('{{') < 0) { + callback(default_value); + return; + } + + callback('Loading...'); + }; + + metaService.getInitialTitle = function(page, callback) { + var route = page.$$route; + initial(route && route.title, Config.REGISTRY_TITLE_SHORT, callback); + }; + + metaService.getInitialDescription = function(page, callback) { + var route = page.$$route; + initial(route && route.description, Config.REGISTRY_TITLE_SHORT, callback); + }; + + metaService.getTitle = function(page, callback) { + var route = page.$$route; + if (!route || !route.title || route.title.indexOf('{{') < 0) { + return; + } + + interpolate(page, route.title, callback); + }; + + metaService.getDescription = function(page, callback) { + var route = page.$$route; + if (!route || !route.description || route.description.indexOf('{{') < 0) { + return; + } + + interpolate(page, route.description, callback); + }; + + return metaService; +}]); From 07a921c92c62ab274a1a7e39f1303ba21426a01d Mon Sep 17 00:00:00 2001 From: Joseph Schorr <joseph.schorr@coreos.com> Date: Fri, 27 Feb 2015 17:22:47 -0500 Subject: [PATCH 4/4] Get dynamic title support working --- static/js/services/meta-service.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/static/js/services/meta-service.js b/static/js/services/meta-service.js index 0cd84fc89..f0b4cb989 100644 --- a/static/js/services/meta-service.js +++ b/static/js/services/meta-service.js @@ -4,15 +4,33 @@ angular.module('quay').factory('MetaService', ['$interpolate', 'Config', '$rootScope', '$interval', function($interpolate, Config, $rootScope, $interval) { var metaService = {}; + var intervals = []; var interpolate = function(page, expr, callback) { - $rootScope.$watch(page.scope, function() { + var previous = ''; + + var currentInterval = $interval(function() { var inter = $interpolate(expr, true, null, true); - callback(inter(page.scope)); - }); + var result = inter(page.scope) + + if (result != previous) { + $interval.cancel(currentInterval); + } + + previous = result; + callback(result); + }, 500); + + intervals.push(currentInterval); }; var initial = function(value, default_value, callback) { + for (var i = 0; i < intervals.length; ++i) { + $interval.cancel(intervals[i]); + } + + intervals = []; + if (!value) { callback(default_value); return;