Add ability to see a build's build pack, including browsing and downloading of the contents if it is a zip
This commit is contained in:
parent
7bf6936154
commit
bc0d51656a
12 changed files with 936 additions and 637 deletions
|
@ -1149,7 +1149,7 @@ def get_repo(namespace, repository):
|
|||
abort(403) # Permission denied
|
||||
|
||||
|
||||
def build_status_view(build_obj):
|
||||
def build_status_view(build_obj, can_write=False):
|
||||
status = build_logs.get_status(build_obj.uuid)
|
||||
return {
|
||||
'id': build_obj.uuid,
|
||||
|
@ -1157,7 +1157,8 @@ def build_status_view(build_obj):
|
|||
'started': build_obj.started,
|
||||
'display_name': build_obj.display_name,
|
||||
'status': status,
|
||||
'resource_key': build_obj.resource_key
|
||||
'resource_key': build_obj.resource_key if can_write else None,
|
||||
'is_writer': can_write
|
||||
}
|
||||
|
||||
|
||||
|
@ -1167,9 +1168,10 @@ def get_repo_builds(namespace, repository):
|
|||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
is_public = model.repository_is_public(namespace, repository)
|
||||
if permission.can() or is_public:
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
builds = model.list_repository_builds(namespace, repository)
|
||||
return jsonify({
|
||||
'builds': [build_status_view(build) for build in builds]
|
||||
'builds': [build_status_view(build, can_write) for build in builds]
|
||||
})
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
@ -1183,7 +1185,29 @@ def get_repo_build_status(namespace, repository, build_uuid):
|
|||
is_public = model.repository_is_public(namespace, repository)
|
||||
if permission.can() or is_public:
|
||||
build = model.get_repository_build(namespace, repository, build_uuid)
|
||||
return jsonify(build_status_view(build))
|
||||
if not build:
|
||||
abort(404)
|
||||
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
return jsonify(build_status_view(build, can_write))
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
||||
|
||||
@api.route('/repository/<path:repository>/build/<build_uuid>/archiveurl',
|
||||
methods=['GET'])
|
||||
@parse_repository_name
|
||||
def get_repo_build_archive_url(namespace, repository, build_uuid):
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
build = model.get_repository_build(namespace, repository, build_uuid)
|
||||
if not build:
|
||||
abort(404)
|
||||
|
||||
url = user_files.get_file_url(build.resource_key)
|
||||
return jsonify({
|
||||
'url': url
|
||||
})
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
||||
|
@ -1249,7 +1273,7 @@ def request_repo_build(namespace, repository):
|
|||
{'repo': repository, 'namespace': namespace,
|
||||
'fileid': dockerfile_id}, repo=repo)
|
||||
|
||||
resp = jsonify(build_status_view(build_request))
|
||||
resp = jsonify(build_status_view(build_request, True))
|
||||
repo_string = '%s/%s' % (namespace, repository)
|
||||
resp.headers['Location'] = url_for('api.get_repo_build_status',
|
||||
repository=repo_string,
|
||||
|
|
|
@ -277,8 +277,8 @@ def populate_database():
|
|||
|
||||
token = model.create_access_token(building, 'write')
|
||||
tag = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name)
|
||||
build = model.create_repository_build(building, token, '123-45-6789', tag,
|
||||
'build-name')
|
||||
build = model.create_repository_build(building, token, '701dcc3724fb4f2ea6c31400528343cd',
|
||||
tag, 'build-name')
|
||||
build.uuid = 'deadbeef-dead-beef-dead-beefdeadbeef'
|
||||
build.save()
|
||||
|
||||
|
|
|
@ -2263,23 +2263,10 @@ p.editable:hover i {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
#changes-tree-container .node rect {
|
||||
cursor: pointer;
|
||||
fill: #fff;
|
||||
fill-opacity: 1;
|
||||
stroke: #fff;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
#changes-tree-container .node .change-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#changes-tree-container .node text {
|
||||
font: 12px sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#changes-tree-container .node.changed text {
|
||||
fill: rgb(73, 100, 209);
|
||||
}
|
||||
|
@ -2293,7 +2280,20 @@ p.editable:hover i {
|
|||
fill: rgb(209, 73, 73);
|
||||
}
|
||||
|
||||
#changes-tree-container path.link {
|
||||
.file-tree-base .node rect {
|
||||
cursor: pointer;
|
||||
fill: #fff;
|
||||
fill-opacity: 1;
|
||||
stroke: #fff;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.file-tree-base .node text {
|
||||
font: 12px sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-tree-base path.link {
|
||||
fill: none;
|
||||
stroke: #9ecae1;
|
||||
stroke-width: 1.5px;
|
||||
|
|
|
@ -791,6 +791,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/build/:buildid/buildpack', {templateUrl: '/static/partials/build-package.html', controller:BuildPackageCtrl, reloadOnSearch: false}).
|
||||
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
|
||||
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html',
|
||||
|
|
|
@ -774,6 +774,128 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
loadViewInfo();
|
||||
}
|
||||
|
||||
function BuildPackageCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $timeout) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var buildid = $routeParams.buildid;
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': buildid
|
||||
};
|
||||
|
||||
$scope.initializeTree = function() {
|
||||
if ($scope.drawn) { return; }
|
||||
|
||||
$scope.drawn = true;
|
||||
$timeout(function() {
|
||||
$scope.tree.draw('file-tree-container');
|
||||
}, 10);
|
||||
};
|
||||
|
||||
var processBuildPack = function(response) {
|
||||
// Try to load as a zip file.
|
||||
var zipFiles = null;
|
||||
var zip = null;
|
||||
try {
|
||||
var zip = new JSZip(response);
|
||||
zipFiles = zip.files;
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
// Find the Dockerfile in the zip file. If there isn't any zip file, then the response
|
||||
// itself (should) be the Dockerfile.
|
||||
if (zipFiles && Object.keys(zipFiles).length) {
|
||||
// Load the dockerfile contents.
|
||||
var dockerfile = zip.file('Dockerfile');
|
||||
if (dockerfile) {
|
||||
$scope.dockerFileContents = dockerfile.asText();
|
||||
}
|
||||
|
||||
// Build the zip file tree.
|
||||
$scope.tree = new FileTree(Object.keys(zipFiles));
|
||||
$($scope.tree).bind('fileClicked', function(e) {
|
||||
var file = zip.file(e.path);
|
||||
if (file) {
|
||||
var blob = new Blob([file.asArrayBuffer()]);
|
||||
saveAs(blob, file.name);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$scope.dockerFileContents = response;
|
||||
}
|
||||
|
||||
$scope.loaded = true;
|
||||
};
|
||||
|
||||
var downloadBuildPack = function() {
|
||||
$scope.downloadProgress = 0;
|
||||
$scope.downloading = true;
|
||||
|
||||
ApiService.getRepoBuildArchiveUrl(null, params).then(function(resp) {
|
||||
startDownload(resp['url']);
|
||||
}, function() {
|
||||
$scope.downloading = false;
|
||||
$scope.downloadError = true;
|
||||
});
|
||||
};
|
||||
|
||||
var startDownload = function(url) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('GET', url, true);
|
||||
if (request.overrideMimeType) {
|
||||
request.overrideMimeType('text/plain; charset=x-user-defined');
|
||||
}
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
var percentLoaded;
|
||||
if (e.lengthComputable) {
|
||||
$scope.downloadProgress = (e.loaded / e.total) * 100;
|
||||
}
|
||||
});
|
||||
};
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.downloading = false;
|
||||
$scope.downloadError = true;
|
||||
});
|
||||
};
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
if (state == 4) {
|
||||
$scope.$apply(function() {
|
||||
$scope.downloading = false;
|
||||
processBuildPack(request.responseText);
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
};
|
||||
|
||||
var getBuildInfo = function() {
|
||||
$scope.repository_build = ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
if (!resp['is_writer']) {
|
||||
$rootScope.title = 'Unknown build';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$rootScope.title = 'Repository Build Pack - ' + resp['display_name'];
|
||||
$scope.repobuild = resp;
|
||||
$scope.repo = {
|
||||
'namespace': namespace,
|
||||
'name': name
|
||||
};
|
||||
|
||||
downloadBuildPack();
|
||||
return resp;
|
||||
});
|
||||
};
|
||||
|
||||
getBuildInfo();
|
||||
}
|
||||
|
||||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
@ -805,7 +927,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
$scope.adjustLogHeight = function() {
|
||||
$('.build-logs').height($(window).height() - 385);
|
||||
$('.build-logs').height($(window).height() - 415);
|
||||
};
|
||||
|
||||
$scope.askRestartBuild = function(build) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Bind polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
|
||||
*/
|
||||
if (!Function.prototype.bind) {
|
||||
if (!Function.prototype.bind) {
|
||||
Function.prototype.bind = function (oThis) {
|
||||
if (typeof this !== "function") {
|
||||
// closest thing possible to the ECMAScript 5 internal IsCallable function
|
||||
|
@ -23,7 +23,7 @@ if (!Function.prototype.bind) {
|
|||
|
||||
return fBound;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var DEPTH_HEIGHT = 100;
|
||||
var DEPTH_WIDTH = 140;
|
||||
|
@ -834,17 +834,7 @@ ImageHistoryTree.prototype.dispose = function() {
|
|||
/**
|
||||
* Based off of http://bl.ocks.org/mbostock/1093025 by Mike Bostock (@mbostock)
|
||||
*/
|
||||
function ImageFileChangeTree(image, changes) {
|
||||
/**
|
||||
* The parent image.
|
||||
*/
|
||||
this.image_ = image;
|
||||
|
||||
/**
|
||||
* The changes being drawn.
|
||||
*/
|
||||
this.changes_ = changes;
|
||||
|
||||
function FileTreeBase() {
|
||||
/**
|
||||
* Counter for creating unique IDs.
|
||||
*/
|
||||
|
@ -860,10 +850,10 @@ function ImageFileChangeTree(image, changes) {
|
|||
/**
|
||||
* Calculates the dimensions of the tree.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.calculateDimensions_ = function(container) {
|
||||
FileTreeBase.prototype.calculateDimensions_ = function(container) {
|
||||
var cw = document.getElementById(container).clientWidth;
|
||||
var barHeight = 20;
|
||||
var ch = (this.changes_.length * barHeight) + 40;
|
||||
var ch = (this.getNodesHeight() * barHeight) + 40;
|
||||
|
||||
var margin = { top: 40, right: 00, bottom: 20, left: 20 };
|
||||
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
||||
|
@ -887,7 +877,7 @@ ImageFileChangeTree.prototype.calculateDimensions_ = function(container) {
|
|||
/**
|
||||
* Updates the dimensions of the tree.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.updateDimensions_ = function() {
|
||||
FileTreeBase.prototype.updateDimensions_ = function() {
|
||||
var container = this.container_;
|
||||
var dimensions = this.calculateDimensions_(container);
|
||||
|
||||
|
@ -917,7 +907,7 @@ ImageFileChangeTree.prototype.updateDimensions_ = function() {
|
|||
/**
|
||||
* Redraws the image change tree to fit the new size.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.notifyResized = function() {
|
||||
FileTreeBase.prototype.notifyResized = function() {
|
||||
this.updateDimensions_();
|
||||
this.update_(this.root_);
|
||||
};
|
||||
|
@ -926,7 +916,7 @@ ImageFileChangeTree.prototype.notifyResized = function() {
|
|||
/**
|
||||
* Disposes of the tree.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.dispose = function() {
|
||||
FileTreeBase.prototype.dispose = function() {
|
||||
var container = this.container_ ;
|
||||
document.getElementById(container).innerHTML = '';
|
||||
};
|
||||
|
@ -935,7 +925,7 @@ ImageFileChangeTree.prototype.dispose = function() {
|
|||
/**
|
||||
* Draws the tree.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.draw = function(container) {
|
||||
FileTreeBase.prototype.draw = function(container) {
|
||||
this.container_ = container;
|
||||
|
||||
var dimensions = this.calculateDimensions_(container);
|
||||
|
@ -954,6 +944,7 @@ ImageFileChangeTree.prototype.draw = function(container) {
|
|||
.projection(function(d) { return [d.y, d.x]; });
|
||||
|
||||
var rootSvg = d3.select("#" + container).append("svg:svg")
|
||||
.attr('class', 'file-tree-base')
|
||||
.attr("width", w)
|
||||
.attr("height", h);
|
||||
|
||||
|
@ -973,14 +964,9 @@ ImageFileChangeTree.prototype.draw = function(container) {
|
|||
/**
|
||||
* Populates the tree and then draws it.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.populateAndDraw_ = function() {
|
||||
// For each change, generate all the node(s) needed for the folders, as well
|
||||
// as the final node (if any) for the file.
|
||||
for (var i = 0; i < this.changes_.length; ++i) {
|
||||
var filepath = this.changes_[i].file;
|
||||
var node = this.buildNodes_(filepath);
|
||||
node.kind = this.changes_[i].kind;
|
||||
}
|
||||
FileTreeBase.prototype.populateAndDraw_ = function() {
|
||||
// Build all the nodes for the file tree.
|
||||
this.buildAllNodes();
|
||||
|
||||
// Sort the children of each node so that folders are on top.
|
||||
var sortByName = function (a, b) {
|
||||
|
@ -1023,7 +1009,7 @@ ImageFileChangeTree.prototype.populateAndDraw_ = function() {
|
|||
/**
|
||||
* Builds all the nodes in the tree.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.buildNodes_ = function(path) {
|
||||
FileTreeBase.prototype.buildNodes_ = function(path) {
|
||||
var parts = path.split('/');
|
||||
for (var i = 0; i < parts.length; ++i) {
|
||||
var currentPath = parts.slice(0, i + 1).join('/');
|
||||
|
@ -1032,7 +1018,8 @@ ImageFileChangeTree.prototype.buildNodes_ = function(path) {
|
|||
|
||||
if (currentPath.length > 0) {
|
||||
var parentPath = parts.slice(0, i).join('/');
|
||||
this.nodeMap_[parentPath]._children.push(this.nodeMap_[currentPath]);
|
||||
var parent = this.buildNodes_(parentPath);
|
||||
parent._children.push(this.nodeMap_[currentPath]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1043,7 +1030,7 @@ ImageFileChangeTree.prototype.buildNodes_ = function(path) {
|
|||
/**
|
||||
* Calculates the count of visible nodes.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.getVisibleCount_ = function(node) {
|
||||
FileTreeBase.prototype.getVisibleCount_ = function(node) {
|
||||
if (node.children) {
|
||||
var count = 1;
|
||||
for (var i = 0; i < node.children.length; ++i) {
|
||||
|
@ -1058,7 +1045,7 @@ ImageFileChangeTree.prototype.getVisibleCount_ = function(node) {
|
|||
/**
|
||||
* Calculates the height for the container.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.getContainerHeight_ = function() {
|
||||
FileTreeBase.prototype.getContainerHeight_ = function() {
|
||||
var dimensions = this.calculateDimensions_(this.container_);
|
||||
var barHeight = this.barHeight_;
|
||||
var height = (this.getVisibleCount_(this.root_) * (barHeight + 2));
|
||||
|
@ -1069,7 +1056,7 @@ ImageFileChangeTree.prototype.getContainerHeight_ = function() {
|
|||
/**
|
||||
* Updates the tree starting at the given source node.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.update_ = function(source) {
|
||||
FileTreeBase.prototype.update_ = function(source) {
|
||||
var that = this;
|
||||
var tree = this.tree_;
|
||||
var vis = this.vis_;
|
||||
|
@ -1117,7 +1104,17 @@ ImageFileChangeTree.prototype.update_ = function(source) {
|
|||
.attr("height", barHeight)
|
||||
.attr("width", barWidth)
|
||||
.style("fill", color)
|
||||
.on("click", function(d) { that.toggle_(d); that.update_(source); });
|
||||
.on("click", function(d) {
|
||||
if (d.kind) {
|
||||
$(that).trigger({
|
||||
'type': 'fileClicked',
|
||||
'path': d.path
|
||||
});
|
||||
return;
|
||||
}
|
||||
that.toggle_(d);
|
||||
that.update_(source);
|
||||
});
|
||||
|
||||
nodeEnter.append("svg:text")
|
||||
.attr("dy", 3.5)
|
||||
|
@ -1153,7 +1150,7 @@ ImageFileChangeTree.prototype.update_ = function(source) {
|
|||
node.select('.main-rect')
|
||||
.attr("y", -barHeight / 2)
|
||||
.attr("height", barHeight)
|
||||
.attr("width", barWidth)
|
||||
.attr("width", barWidth);
|
||||
|
||||
node.select('.fo')
|
||||
.attr("x", function(d) { return d.kind ? barWidth - 18 : 0; })
|
||||
|
@ -1166,13 +1163,7 @@ ImageFileChangeTree.prototype.update_ = function(source) {
|
|||
return '<i class="' + folder + '"></i>';
|
||||
}
|
||||
|
||||
var icon = {
|
||||
'added': 'plus-square',
|
||||
'removed': 'minus-square',
|
||||
'changed': 'pencil-square'
|
||||
};
|
||||
|
||||
return '<i class="change-icon fa fa-' + icon[d.kind] + '"></i>';
|
||||
return that.determineIcon(d);
|
||||
});
|
||||
|
||||
// Transition exiting nodes to the parent's new position.
|
||||
|
@ -1226,7 +1217,7 @@ ImageFileChangeTree.prototype.update_ = function(source) {
|
|||
/**
|
||||
* Toggles children of a node.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.toggle_ = function(d) {
|
||||
FileTreeBase.prototype.toggle_ = function(d) {
|
||||
if (d.children) {
|
||||
d._children = d.children;
|
||||
d.children = null;
|
||||
|
@ -1237,6 +1228,101 @@ ImageFileChangeTree.prototype.toggle_ = function(d) {
|
|||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function ImageFileChangeTree(image, changes) {
|
||||
FileTreeBase.apply(this);
|
||||
|
||||
/**
|
||||
* The parent image.
|
||||
*/
|
||||
this.image_ = image;
|
||||
|
||||
/**
|
||||
* The changes being drawn.
|
||||
*/
|
||||
this.changes_ = changes;
|
||||
}
|
||||
|
||||
$.extend(ImageFileChangeTree.prototype, FileTreeBase.prototype);
|
||||
|
||||
/**
|
||||
* Builds all the file nodes from the changes.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.buildAllNodes = function() {
|
||||
for (var i = 0; i < this.changes_.length; ++i) {
|
||||
var filepath = this.changes_[i].file;
|
||||
var node = this.buildNodes_(filepath);
|
||||
node.kind = this.changes_[i].kind;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines the icon for a node.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.determineIcon = function(d) {
|
||||
var icon = {
|
||||
'added': 'plus-square',
|
||||
'removed': 'minus-square',
|
||||
'changed': 'pencil-square'
|
||||
};
|
||||
|
||||
return '<i class="change-icon fa fa-' + icon[d.kind] + '"></i>';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the max height of the tree.
|
||||
*/
|
||||
ImageFileChangeTree.prototype.getNodesHeight = function() {
|
||||
return this.changes_.length;
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function FileTree(files) {
|
||||
FileTreeBase.apply(this);
|
||||
|
||||
/**
|
||||
* The set of file paths found in the tree.
|
||||
*/
|
||||
this.files_ = files;
|
||||
}
|
||||
|
||||
$.extend(FileTree.prototype, FileTreeBase.prototype);
|
||||
|
||||
|
||||
/**
|
||||
* Builds all the file nodes from the paths.
|
||||
*/
|
||||
FileTree.prototype.buildAllNodes = function() {
|
||||
for (var i = 0; i < this.files_.length; ++i) {
|
||||
var filepath = this.files_[i];
|
||||
if (filepath[filepath.length - 1] == '/') { continue; }
|
||||
var node = this.buildNodes_(filepath);
|
||||
node.kind = 'file';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines the icon for a node.
|
||||
*/
|
||||
FileTree.prototype.determineIcon = function(d) {
|
||||
return '';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the max height of the tree.
|
||||
*/
|
||||
FileTree.prototype.getNodesHeight = function() {
|
||||
return this.files_.length;
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
|
|
14
static/lib/jszip.min.js
vendored
Executable file
14
static/lib/jszip.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
45
static/partials/build-package.html
Normal file
45
static/partials/build-package.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
<div class="resource-view" resource="repository_build" error-message="'No matching repository build found'"></div>
|
||||
<div class="container repo repo-build" ng-show="accessDenied">
|
||||
You do not have permission to view this page
|
||||
</div>
|
||||
<div class="container repo repo-build repo-build-pack" ng-show="repobuild">
|
||||
<div class="header row">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/build' }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||
<h3>
|
||||
<span class="repo-circle no-background" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo" subsection-icon="'fa-tasks'" subsection="repobuild.display_name"></span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="downloading">
|
||||
Downloading build pack:
|
||||
<div class="progress" class="active progress-striped">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ downloadProgress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + downloadProgress + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="downloadError">
|
||||
Error: Could not download the build pack
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="loaded">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#dockerfile">Dockerfile</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()" ng-show="tree">All Files</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Dockerfile view -->
|
||||
<div class="tab-pane active" id="dockerfile">
|
||||
<pre ng-show="dockerFileContents">{{ dockerFileContents }}</pre>
|
||||
<span ng-show="!dockerFileContents">No Dockerfile found in the build pack</span>
|
||||
</div>
|
||||
|
||||
<!-- File tree -->
|
||||
<div class="tab-pane" id="tree">
|
||||
<div id="file-tree-container" class="tree-container" onresize="tree && drawn && tree.notifyResized()"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -43,6 +43,12 @@
|
|||
<div class="timing">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
Started: <span am-time-ago="build.started || 0"></span>
|
||||
<span style="display: inline-block; margin-left: 20px" ng-show="currentBuild.resource_key">
|
||||
<i class="fa fa-archive"></i>
|
||||
<a href="/repository/{{ repo.namespace }}/{{ repo.name }}/build/{{ currentBuild.id }}/buildpack"
|
||||
style="display: inline-block; margin-left: 6px" bs-tooltip="tooltip.title"
|
||||
title="View the uploaded build package for this build">Build Package</a>
|
||||
</span>
|
||||
</div>
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
<span class="build-message" phase="build.phase"></span>
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.2.0/js/bootstrap-datepicker.min.js"></script>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
|
||||
|
||||
<script src="static/lib/nv.d3.min.js"></script>
|
||||
|
||||
<script src="static/lib/ZeroClipboard.min.js"></script>
|
||||
|
@ -32,6 +32,7 @@
|
|||
<script src="static/lib/d3-tip.js" charset="utf-8"></script>
|
||||
<script src="static/lib/browser-chrome.js"></script>
|
||||
|
||||
<script src="static/lib/jszip.min.js"></script>
|
||||
<script src="static/lib/Blob.js"></script>
|
||||
<script src="static/lib/FileSaver.js"></script>
|
||||
<script src="static/lib/jquery.base64.min.js"></script>
|
||||
|
|
Binary file not shown.
|
@ -250,7 +250,7 @@ class TestGetMatchingEntities(ApiTestCase):
|
|||
|
||||
json = self.getJsonResponse('api.get_matching_entities',
|
||||
params=dict(prefix='o', namespace=ORGANIZATION,
|
||||
includeTeams=True))
|
||||
includeTeams='true'))
|
||||
|
||||
names = set([r['name'] for r in json['results']])
|
||||
assert 'outsideorg' in names
|
||||
|
@ -261,7 +261,7 @@ class TestGetMatchingEntities(ApiTestCase):
|
|||
|
||||
json = self.getJsonResponse('api.get_matching_entities',
|
||||
params=dict(prefix='o', namespace=ORGANIZATION,
|
||||
includeTeams=True))
|
||||
includeTeams='true'))
|
||||
|
||||
names = set([r['name'] for r in json['results']])
|
||||
assert 'outsideorg' in names
|
||||
|
@ -684,12 +684,12 @@ class TestFindRepos(ApiTestCase):
|
|||
|
||||
class TestListRepos(ApiTestCase):
|
||||
def test_listrepos_asguest(self):
|
||||
json = self.getJsonResponse('api.list_repos', params=dict(public=True))
|
||||
assert len(json['repositories']) == 0
|
||||
json = self.getJsonResponse('api.list_repos', params=dict(public='true'))
|
||||
assert len(json['repositories']) > 1
|
||||
|
||||
def test_listrepos_orgmember(self):
|
||||
self.login(READ_ACCESS_USER)
|
||||
json = self.getJsonResponse('api.list_repos', params=dict(public=True))
|
||||
json = self.getJsonResponse('api.list_repos', params=dict(public='true'))
|
||||
assert len(json['repositories']) > 1
|
||||
|
||||
def test_listrepos_filter(self):
|
||||
|
|
Reference in a new issue