Merge pull request #2724 from coreos-inc/multistage-build-ui
Add support for multistage Docker build in build UI
This commit is contained in:
commit
3b73695c04
25 changed files with 229 additions and 450 deletions
16
static/css/directives/ui/build-log-command.css
Normal file
16
static/css/directives/ui/build-log-command.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.build-log-command-element.multistep-seperator {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-log-command-element .from-name {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 22px;
|
||||||
|
background-color: #263945;
|
||||||
|
display: inline-block;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
|
@ -87,16 +87,15 @@
|
||||||
.build-logs-view .container-header .label {
|
.build-logs-view .container-header .label {
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-right: 4px;
|
margin-left: 2px;
|
||||||
|
margin-right: 10px;
|
||||||
width: 86px;
|
width: 86px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
border-right: 4px solid #aaa;
|
border-right: 4px solid #aaa;
|
||||||
background-color: #717171;
|
background-color: #717171;
|
||||||
|
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 4px;
|
|
||||||
left: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.build-logs-view .dockerfile-command {
|
.build-logs-view .dockerfile-command {
|
||||||
|
@ -112,10 +111,6 @@
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.build-logs-view .container-header .container-content.build-log-command {
|
|
||||||
padding-left: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.build-logs-view .log-entry {
|
.build-logs-view .log-entry {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
.image-info-sidebar .image-comment {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-sidebar .image-section {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-sidebar .image-section .tag {
|
|
||||||
margin: 2px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-sidebar .image-section .section-icon {
|
|
||||||
float: left;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-left: -4px;
|
|
||||||
margin-right: 14px;
|
|
||||||
color: #bbb;
|
|
||||||
width: 18px;
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-sidebar .image-section .section-info {
|
|
||||||
padding: 4px;
|
|
||||||
padding-left: 6px;
|
|
||||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
|
|
||||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
|
|
||||||
vertical-align: middle;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-sidebar .image-section .section-info-with-dropdown {
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-sidebar .image-section .dropdown {
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
bottom: 2px;
|
|
||||||
right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-sidebar .image-section .dropdown-button {
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
top: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
|
|
||||||
background: white;
|
|
||||||
padding: 4px;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
|
@ -222,49 +222,6 @@
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dockerfile-view {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background: #F7F6F6;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dockerfile-view .entry {
|
|
||||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dockerfile-view .entry.comment {
|
|
||||||
color: rgb(82, 172, 82);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dockerfile-command {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dockerfile-command .command-title {
|
|
||||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
|
||||||
padding-left: 90px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dockerfile-command .label {
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
padding-top: 4px;
|
|
||||||
text-align: right;
|
|
||||||
margin-right: 4px;
|
|
||||||
width: 86px;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
border-right: 4px solid #aaa;
|
|
||||||
background-color: #333;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codetooltipcontainer .tooltip-inner {
|
.codetooltipcontainer .tooltip-inner {
|
||||||
white-space:pre;
|
white-space:pre;
|
||||||
max-width:none;
|
max-width:none;
|
||||||
|
@ -2968,26 +2925,6 @@ pre.command:before {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label.FROM {
|
|
||||||
border-color: #5bc0de !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label.CMD, .label.EXPOSE, .label.ENTRYPOINT {
|
|
||||||
border-color: #428bca !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label.RUN, .label.ADD {
|
|
||||||
border-color: #5cb85c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label.ENV, .label.VOLUME, .label.USER, .label.WORKDIR {
|
|
||||||
border-color: #f0ad4e !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label.MAINTAINER {
|
|
||||||
border-color: #aaa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-select {
|
.dropdown-select {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
<span class="dockerfile-command command" command="getWithoutStep(command.message)"></span>
|
<div class="build-log-command-element" ng-class="::{'multistep-seperator': !!isSecondaryFrom(command.message) || fromName(command.message)}">
|
||||||
|
<span class="from-name" ng-if="::!!fromName(command.message)">{{ ::fromName(command.message) }}</span>
|
||||||
|
<dockerfile-command class="command" command="getWithoutStep(command.message)"></dockerfile-command>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<span class="dockerfile-command-element" bindonce>
|
|
||||||
<span class="label" bo-class="getCommandKind(command)" bo-show="getCommandKind(command)"
|
|
||||||
bo-text="getCommandKind(command)">
|
|
||||||
</span>
|
|
||||||
<span class="command-title" bo-html="getCommandTitleHtml(command)"></span>
|
|
||||||
</span>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<div class="dockerfile-view-element">
|
|
||||||
<div class="dockerfile-line" ng-repeat="line in lines">
|
|
||||||
<div ng-switch on="line.kind">
|
|
||||||
<div class="command entry" ng-switch-when="command">
|
|
||||||
<div class="dockerfile-command" command="line.text"></div>
|
|
||||||
</div>
|
|
||||||
<div class="comment entry" ng-switch-when="comment">
|
|
||||||
{{ line.text || ' ' }}
|
|
||||||
</div>
|
|
||||||
<div class="text entry" ng-switch-default>
|
|
||||||
{{ line.text || ' ' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -164,9 +164,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
|
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
|
||||||
<span bo-if="feature.imageCommand">
|
<span bo-if="feature.imageCommand">
|
||||||
<span data-title="{{ feature.imageCommand }}" data-container="body" bs-tooltip>
|
<image-command command="feature.imageCommand"></image-command>
|
||||||
<span class="dockerfile-command" command="feature.imageCommand"></span>
|
|
||||||
</span>
|
|
||||||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ feature.imageId }}"><i class="fa fa-archive"></i></a>
|
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ feature.imageId }}"><i class="fa fa-archive"></i></a>
|
||||||
</span>
|
</span>
|
||||||
<span bo-if="!feature.imageCommand">
|
<span bo-if="!feature.imageCommand">
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
<div class="image-info-sidebar-element">
|
|
||||||
<!-- Comment -->
|
|
||||||
<div class="image-comment" ng-if="imageData.comment">
|
|
||||||
<blockquote style="margin-top: 10px;">
|
|
||||||
<markdown-view content="imageData.comment"></markdown-view>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Image ID -->
|
|
||||||
<div class="image-section">
|
|
||||||
<i class="fa fa-code section-icon" bs-tooltip="tooltip.title" data-title="Full Image ID"></i>
|
|
||||||
<span class="section-info">
|
|
||||||
<a class="current-image-link" ng-href="{{ tracker.imageLink(image) }}">
|
|
||||||
{{ imageData.id }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tags -->
|
|
||||||
<div class="image-section">
|
|
||||||
<i class="fa fa-tag section-icon" data-title="Current Tags" bs-tooltip></i>
|
|
||||||
<span class="section-info section-info-with-dropdown">
|
|
||||||
<a class="label tag label-default" ng-repeat="tag in getTags(imageData)"
|
|
||||||
ng-click="tagSelected({'tag': tag})">
|
|
||||||
{{ tag }}
|
|
||||||
</a>
|
|
||||||
<span style="color: #ccc;" ng-if="!getTags(imageData).length">(No Tags)</span>
|
|
||||||
|
|
||||||
<div class="dropdown" data-placement="top"
|
|
||||||
ng-if="tracker.repository.can_write || getTags(imageData)">
|
|
||||||
<a class="dropdown-button" data-toggle="dropdown"
|
|
||||||
bs-tooltip="tooltip.title" data-title="Manage Tags"
|
|
||||||
data-container="body">
|
|
||||||
<b class="caret"></b>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<ul class="dropdown-menu pull-right">
|
|
||||||
<li ng-repeat="tag in getTags(imageData)">
|
|
||||||
<a ng-click="tagSelected({'tag': tag})">
|
|
||||||
<i class="fa fa-tag"></i>{{ tag }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="divider" role="presentation"
|
|
||||||
ng-if="tracker.repository.can_write && imageData.tags"></li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
ng-click="addTagRequested({'image': image})"
|
|
||||||
ng-if="tracker.repository.can_write">
|
|
||||||
<i class="fa fa-plus"></i>Add New Tag
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Command -->
|
|
||||||
<div class="image-section" ng-if="imageData.command && imageData.command.length">
|
|
||||||
<i class="fa fa-terminal section-icon" data-title="Image Command" bs-tooltip></i>
|
|
||||||
<span class="section-info">
|
|
||||||
<span class="formatted-command trimmed"
|
|
||||||
data-html="true"
|
|
||||||
data-title="{{ getTooltipCommand(imageData) }}"
|
|
||||||
data-placement="top" bs-tooltip>{{ getFormattedCommand(imageData) }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Created -->
|
|
||||||
<div class="image-section">
|
|
||||||
<i class="fa fa-calendar section-icon" data-title="Created" bs-tooltip></i>
|
|
||||||
<span class="section-info">
|
|
||||||
<dd am-time-ago="parseDate(imageData.created)"></dd>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Size -->
|
|
||||||
<div class="image-section">
|
|
||||||
<i class="fa fa-cloud-upload section-icon"
|
|
||||||
data-title="The amount of data sent between Docker and the registry when pushing/pulling"
|
|
||||||
bs-tooltip></i>
|
|
||||||
<span class="section-info">{{ imageData.size | bytes }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Locations -->
|
|
||||||
<div class="image-section" ng-if="imageData.locations">
|
|
||||||
<i class="fa fa-map-marker section-icon"
|
|
||||||
data-title="The geographic region(s) in which this image data is located"
|
|
||||||
bs-tooltip></i>
|
|
||||||
<span class="section-info">
|
|
||||||
<span class="location-view" location="location"
|
|
||||||
ng-repeat="location in imageData.locations"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -5,9 +5,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-command">
|
<div class="image-command">
|
||||||
<div class="nondocker-command" ng-if="!getDockerfileCommand(image.command)">{{ image.command.join(' ') }}</div>
|
<image-command command="image.command"></image-command>
|
||||||
<div class="dockerfile-command" command="getDockerfileCommand(image.command)"
|
|
||||||
ng-if="getDockerfileCommand(image.command)"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="image-layer-dot"></div>
|
<div class="image-layer-dot"></div>
|
||||||
<div class="image-layer-line"></div>
|
<div class="image-layer-line"></div>
|
||||||
|
|
|
@ -152,9 +152,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
|
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
|
||||||
<span bo-if="vuln.imageCommand">
|
<span bo-if="vuln.imageCommand">
|
||||||
<span data-title="{{ vuln.imageCommand }}" data-container="body" bs-tooltip>
|
<image-command command="vuln.imageCommand"></image-command>
|
||||||
<span class="dockerfile-command" command="vuln.imageCommand"></span>
|
|
||||||
</span>
|
|
||||||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ vuln.imageId }}"><i class="fa fa-archive"></i></a>
|
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ vuln.imageId }}"><i class="fa fa-archive"></i></a>
|
||||||
</span>
|
</span>
|
||||||
<span bo-if="!vuln.imageCommand">
|
<span bo-if="!vuln.imageCommand">
|
||||||
|
|
|
@ -9,7 +9,7 @@ angular.module('quay').directive('buildLogCommand', function () {
|
||||||
transclude: false,
|
transclude: false,
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'command': '=command'
|
'command': '<command'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element) {
|
||||||
$scope.getWithoutStep = function(fullTitle) {
|
$scope.getWithoutStep = function(fullTitle) {
|
||||||
|
@ -20,6 +20,29 @@ angular.module('quay').directive('buildLogCommand', function () {
|
||||||
|
|
||||||
return $.trim(fullTitle.substring(colon + 1));
|
return $.trim(fullTitle.substring(colon + 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.isSecondaryFrom = function(fullTitle) {
|
||||||
|
if (!fullTitle) { return false; }
|
||||||
|
|
||||||
|
var command = $scope.getWithoutStep(fullTitle);
|
||||||
|
return command.indexOf('FROM ') == 0 && fullTitle.indexOf('Step 1 ') < 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.fromName = function(fullTitle) {
|
||||||
|
var command = $scope.getWithoutStep(fullTitle);
|
||||||
|
if (command.indexOf('FROM ') != 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = command.split(' ');
|
||||||
|
for (var i = 0; i < parts.length - 1; i++) {
|
||||||
|
var part = parts[i];
|
||||||
|
if ($.trim(part) == 'as') {
|
||||||
|
return parts[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
/**
|
|
||||||
* An element which displays a Dockerfile command nicely formatted, with optional link to the
|
|
||||||
* image (for FROM commands that link to us or to the DockerHub).
|
|
||||||
*/
|
|
||||||
angular.module('quay').directive('dockerfileCommand', function () {
|
|
||||||
var directiveDefinitionObject = {
|
|
||||||
priority: 0,
|
|
||||||
templateUrl: '/static/directives/dockerfile-command.html',
|
|
||||||
replace: false,
|
|
||||||
transclude: false,
|
|
||||||
restrict: 'C',
|
|
||||||
scope: {
|
|
||||||
'command': '=command'
|
|
||||||
},
|
|
||||||
controller: function($scope, $element, UtilService, Config) {
|
|
||||||
var registryHandlers = {
|
|
||||||
'quay.io': function(pieces) {
|
|
||||||
var rnamespace = pieces[pieces.length - 2];
|
|
||||||
var rname = pieces[pieces.length - 1].split(':')[0];
|
|
||||||
return '/repository/' + rnamespace + '/' + rname + '/';
|
|
||||||
},
|
|
||||||
|
|
||||||
'': function(pieces) {
|
|
||||||
var rnamespace = pieces.length == 1 ? '_' : 'u/' + pieces[0];
|
|
||||||
var rname = pieces[pieces.length - 1].split(':')[0];
|
|
||||||
return 'https://registry.hub.docker.com/' + rnamespace + '/' + rname + '/';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];
|
|
||||||
|
|
||||||
var kindHandlers = {
|
|
||||||
'FROM': function(title) {
|
|
||||||
var pieces = title.split('/');
|
|
||||||
var registry = pieces.length < 3 ? '' : pieces[0];
|
|
||||||
if (!registryHandlers[registry]) {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '" target="_blank">' + title + '</a>';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getCommandKind = function(title) {
|
|
||||||
var space = title.indexOf(' ');
|
|
||||||
return title.substring(0, space);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getCommandTitleHtml = function(title) {
|
|
||||||
var space = title.indexOf(' ');
|
|
||||||
if (space <= 0) {
|
|
||||||
return UtilService.textToSafeHtml(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
var kind = $scope.getCommandKind(title);
|
|
||||||
var sanitized = UtilService.textToSafeHtml(title.substring(space + 1));
|
|
||||||
|
|
||||||
var handler = kindHandlers[kind || ''];
|
|
||||||
if (handler) {
|
|
||||||
return handler(sanitized);
|
|
||||||
} else {
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return directiveDefinitionObject;
|
|
||||||
});
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
.label.FROM {
|
||||||
|
border-color: #5bc0de !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.ARG {
|
||||||
|
border-color: #eaef4d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.ONBUILD {
|
||||||
|
border-color: #6813d8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.CMD, .label.EXPOSE, .label.ENTRYPOINT {
|
||||||
|
border-color: #428bca !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.RUN, .label.ADD, .label.COPY {
|
||||||
|
border-color: #5cb85c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.ENV, .label.VOLUME, .label.USER, .label.WORKDIR, .label.HEALTHCHECK, .label.STOPSIGNAL, .label.SHELL {
|
||||||
|
border-color: #f0ad4e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.MAINTAINER {
|
||||||
|
border-color: #aaa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dockerfile-command {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dockerfile-command .command-title {
|
||||||
|
font-family: Consolas, "Lucida Console", Monaco, monospace !important;
|
||||||
|
padding-left: 90px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dockerfile-command .command-title a {
|
||||||
|
color: #5bc0de;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dockerfile-command .label {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
padding-top: 4px;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 4px;
|
||||||
|
width: 86px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
border-right: 4px solid #aaa;
|
||||||
|
background-color: #333;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: -4px;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<span class="dockerfile-command dockerfile-command-element">
|
||||||
|
<span class="label" ng-class="::$ctrl.getCommandKind($ctrl.command)"
|
||||||
|
ng-if="::$ctrl.getCommandKind($ctrl.command)">
|
||||||
|
{{ ::$ctrl.getCommandKind($ctrl.command) }}
|
||||||
|
</span>
|
||||||
|
<span class="command-title" ng-bind-html="::$ctrl.getCommandTitleHtml($ctrl.command)"></span>
|
||||||
|
</span>
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||||
|
import './dockerfile-command.component.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component which displays a Dockerfile command, nicely formatted.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'dockerfile-command',
|
||||||
|
templateUrl: '/static/js/directives/ui/dockerfile-command/dockerfile-command.component.html',
|
||||||
|
})
|
||||||
|
export class DockerfileCommandComponent {
|
||||||
|
@Input('<') public command: string;
|
||||||
|
|
||||||
|
private registryHandlers: {[domain: string]: Function};
|
||||||
|
|
||||||
|
constructor (@Inject('Config') Config: any, @Inject('UtilService') private utilService: any) {
|
||||||
|
var registryHandlers = {
|
||||||
|
'quay.io': function(pieces) {
|
||||||
|
var rnamespace = pieces[pieces.length - 2];
|
||||||
|
var rname = pieces[pieces.length - 1].split(':')[0];
|
||||||
|
return '/repository/' + rnamespace + '/' + rname + '/';
|
||||||
|
},
|
||||||
|
|
||||||
|
'': function(pieces) {
|
||||||
|
var rnamespace = pieces.length == 1 ? '_' : 'u/' + pieces[0];
|
||||||
|
var rname = pieces[pieces.length - 1].split(':')[0];
|
||||||
|
return 'https://registry.hub.docker.com/' + rnamespace + '/' + rname + '/';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];
|
||||||
|
this.registryHandlers = registryHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCommandKind(command: string): string {
|
||||||
|
if (!command) { return ''; }
|
||||||
|
|
||||||
|
var space = command.indexOf(' ');
|
||||||
|
return command.substring(0, space);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCommandTitleHtml(command: string): string {
|
||||||
|
if (!command) { return ''; }
|
||||||
|
|
||||||
|
var kindHandlers = {
|
||||||
|
'FROM': (command) => {
|
||||||
|
var parts = command.split(' ');
|
||||||
|
var pieces = parts[0].split('/');
|
||||||
|
var registry = pieces.length < 3 ? '' : pieces[0];
|
||||||
|
if (!this.registryHandlers[registry]) {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<a href="' + this.registryHandlers[registry](pieces) + '" target="_blank">' + parts[0] + '</a> ' + (parts.splice(1).join(' '));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var space = command.indexOf(' ');
|
||||||
|
if (space <= 0) {
|
||||||
|
return this.utilService.textToSafeHtml(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
var kind = this.getCommandKind(command);
|
||||||
|
var sanitized = this.utilService.textToSafeHtml(command.substring(space + 1));
|
||||||
|
|
||||||
|
var handler = kindHandlers[kind || ''];
|
||||||
|
if (handler) {
|
||||||
|
return handler(sanitized);
|
||||||
|
} else {
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
/**
|
|
||||||
* An element which displays the contents of a Dockerfile in a nicely formatted way.
|
|
||||||
*/
|
|
||||||
angular.module('quay').directive('dockerfileView', function () {
|
|
||||||
var directiveDefinitionObject = {
|
|
||||||
priority: 0,
|
|
||||||
templateUrl: '/static/directives/dockerfile-view.html',
|
|
||||||
replace: false,
|
|
||||||
transclude: false,
|
|
||||||
restrict: 'C',
|
|
||||||
scope: {
|
|
||||||
'contents': '=contents'
|
|
||||||
},
|
|
||||||
controller: function($scope, $element, UtilService) {
|
|
||||||
$scope.$watch('contents', function(contents) {
|
|
||||||
$scope.lines = [];
|
|
||||||
|
|
||||||
var lines = contents ? contents.split('\n') : [];
|
|
||||||
for (var i = 0; i < lines.length; ++i) {
|
|
||||||
var line = $.trim(lines[i]);
|
|
||||||
var kind = 'text';
|
|
||||||
if (line && line[0] == '#') {
|
|
||||||
kind = 'comment';
|
|
||||||
} else if (line.match(/^([A-Z]+\s)/)) {
|
|
||||||
kind = 'command';
|
|
||||||
}
|
|
||||||
|
|
||||||
var lineInfo = {
|
|
||||||
'text': line,
|
|
||||||
'kind': kind
|
|
||||||
};
|
|
||||||
$scope.lines.push(lineInfo);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return directiveDefinitionObject;
|
|
||||||
});
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="image-command-element">
|
||||||
|
<div class="nondocker-command" ng-if="!$ctrl.getDockerfileCommand($ctrl.command)">{{ ::$ctrl.command.join(' ') }}</div>
|
||||||
|
<dockerfile-command command="::$ctrl.getDockerfileCommand($ctrl.command)"
|
||||||
|
ng-if="$ctrl.getDockerfileCommand($ctrl.command)"></dockerfile-command>
|
||||||
|
</div>
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component which displays an image's command, nicely formatted.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'image-command',
|
||||||
|
templateUrl: '/static/js/directives/ui/image-command/image-command.component.html',
|
||||||
|
})
|
||||||
|
export class ImageCommandComponent {
|
||||||
|
@Input('<') public command: string;
|
||||||
|
|
||||||
|
private getDockerfileCommand(command: string[]): string {
|
||||||
|
if (!command || !command.length) { return ''; }
|
||||||
|
command = command.join(' ').split(' ');
|
||||||
|
|
||||||
|
// ["/bin/sh", "-c", "#(nop)", "RUN", "foo"]
|
||||||
|
if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; }
|
||||||
|
|
||||||
|
if (command[2].trim() != '#(nop)') {
|
||||||
|
return 'RUN ' + command.slice(2).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.slice(3).join(' ');
|
||||||
|
};
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ angular.module('quay').directive('imageFeatureView', function () {
|
||||||
'image': '=image',
|
'image': '=image',
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, ImageMetadataService, TableService) {
|
controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, TableService) {
|
||||||
$scope.options = {
|
$scope.options = {
|
||||||
'filter': null,
|
'filter': null,
|
||||||
'predicate': 'fixableScore',
|
'predicate': 'fixableScore',
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
/**
|
|
||||||
* An element which displays sidebar information for a image. Primarily used in the repo view.
|
|
||||||
*/
|
|
||||||
angular.module('quay').directive('imageInfoSidebar', function () {
|
|
||||||
var directiveDefinitionObject = {
|
|
||||||
priority: 0,
|
|
||||||
templateUrl: '/static/directives/image-info-sidebar.html',
|
|
||||||
replace: false,
|
|
||||||
transclude: true,
|
|
||||||
restrict: 'C',
|
|
||||||
scope: {
|
|
||||||
'tracker': '=tracker',
|
|
||||||
'image': '=image',
|
|
||||||
'imageLoader': '=imageLoader',
|
|
||||||
|
|
||||||
'tagSelected': '&tagSelected',
|
|
||||||
'addTagRequested': '&addTagRequested'
|
|
||||||
},
|
|
||||||
controller: function($scope, $element, ImageMetadataService) {
|
|
||||||
$scope.$watch('image', function(image) {
|
|
||||||
if (!image || !$scope.tracker) { return; }
|
|
||||||
$scope.imageData = $scope.tracker.lookupImage(image);
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.parseDate = function(dateString) {
|
|
||||||
return Date.parse(dateString);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getTags = function(imageData) {
|
|
||||||
return $scope.imageLoader.getTagsForImage(imageData);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return directiveDefinitionObject;
|
|
||||||
});
|
|
|
@ -13,9 +13,7 @@ angular.module('quay').directive('imageViewLayer', function () {
|
||||||
'image': '=image',
|
'image': '=image',
|
||||||
'images': '=images'
|
'images': '=images'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ImageMetadataService) {
|
controller: function($scope, $element) {
|
||||||
$scope.getDockerfileCommand = ImageMetadataService.getDockerfileCommand;
|
|
||||||
|
|
||||||
$scope.getClass = function() {
|
$scope.getClass = function() {
|
||||||
var index = $.inArray($scope.image, $scope.images);
|
var index = $.inArray($scope.image, $scope.images);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
|
||||||
'image': '=image',
|
'image': '=image',
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $routeParams, Config, ApiService, VulnerabilityService, ViewArray, ImageMetadataService, TableService) {
|
controller: function($scope, $element, $routeParams, Config, ApiService, VulnerabilityService, ViewArray, TableService) {
|
||||||
$scope.options = {
|
$scope.options = {
|
||||||
'filter': null,
|
'filter': null,
|
||||||
'fixableVulns': $routeParams['fixable'] == 'true',
|
'fixableVulns': $routeParams['fixable'] == 'true',
|
||||||
|
|
|
@ -29,6 +29,8 @@ import { MarkdownInputComponent } from './directives/ui/markdown/markdown-input.
|
||||||
import { MarkdownViewComponent } from './directives/ui/markdown/markdown-view.component';
|
import { MarkdownViewComponent } from './directives/ui/markdown/markdown-view.component';
|
||||||
import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-toolbar.component';
|
import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-toolbar.component';
|
||||||
import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component';
|
import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component';
|
||||||
|
import { DockerfileCommandComponent } from './directives/ui/dockerfile-command/dockerfile-command.component';
|
||||||
|
import { ImageCommandComponent } from './directives/ui/image-command/image-command.component';
|
||||||
import { BrowserPlatform, browserPlatform } from './constants/platform.constant';
|
import { BrowserPlatform, browserPlatform } from './constants/platform.constant';
|
||||||
import { ManageTriggerComponent } from './directives/ui/manage-trigger/manage-trigger.component';
|
import { ManageTriggerComponent } from './directives/ui/manage-trigger/manage-trigger.component';
|
||||||
import { ClipboardCopyDirective } from './directives/ui/clipboard-copy/clipboard-copy.directive';
|
import { ClipboardCopyDirective } from './directives/ui/clipboard-copy/clipboard-copy.directive';
|
||||||
|
@ -68,6 +70,8 @@ import * as Clipboard from 'clipboard';
|
||||||
MarkdownToolbarComponent,
|
MarkdownToolbarComponent,
|
||||||
MarkdownEditorComponent,
|
MarkdownEditorComponent,
|
||||||
SearchBoxComponent,
|
SearchBoxComponent,
|
||||||
|
DockerfileCommandComponent,
|
||||||
|
ImageCommandComponent,
|
||||||
TypeaheadDirective,
|
TypeaheadDirective,
|
||||||
ManageTriggerComponent,
|
ManageTriggerComponent,
|
||||||
ClipboardCopyDirective,
|
ClipboardCopyDirective,
|
||||||
|
|
|
@ -1,28 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Helper service for returning information extracted from repository image metadata.
|
* Helper service for returning information extracted from repository image metadata.
|
||||||
*/
|
*/
|
||||||
angular.module('quay').factory('ImageMetadataService', ['UtilService', function(UtilService) {
|
angular.module('quay').factory('ImageMetadataService', [function() {
|
||||||
var metadataService = {};
|
var metadataService = {};
|
||||||
metadataService.getFormattedCommand = function(image) {
|
|
||||||
if (!image || !image.command || !image.command.length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
var getCommandStr = function(command) {
|
|
||||||
// Handle /bin/sh commands specially.
|
|
||||||
if (command.length > 2 && command[0] == '/bin/sh' && command[1] == '-c') {
|
|
||||||
return command[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
return command.join(' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
return getCommandStr(image.command);
|
|
||||||
};
|
|
||||||
|
|
||||||
metadataService.getEscapedFormattedCommand = function(image) {
|
|
||||||
return UtilService.textToSafeHtml(metadataService.getFormattedCommand(image));
|
|
||||||
};
|
|
||||||
|
|
||||||
metadataService.getImageCommand = function(image, imageId) {
|
metadataService.getImageCommand = function(image, imageId) {
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
@ -44,22 +24,7 @@ angular.module('quay').factory('ImageMetadataService', ['UtilService', function(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataService.getDockerfileCommand(found.command);
|
return found.command;
|
||||||
};
|
|
||||||
|
|
||||||
metadataService.getDockerfileCommand = function(command) {
|
|
||||||
if (!command) { return ''; }
|
|
||||||
command = command.join(' ').split(' ');
|
|
||||||
|
|
||||||
// ["/bin/sh", "-c", "#(nop)", "RUN", "foo"]
|
|
||||||
if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; }
|
|
||||||
|
|
||||||
var commandPrefix = '#(nop)';
|
|
||||||
if (command[2] != commandPrefix) {
|
|
||||||
return 'RUN ' + command.slice(2).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return command.slice(3).join(' ');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return metadataService;
|
return metadataService;
|
||||||
|
|
Reference in a new issue