mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2025-04-26 15:09:53 +03:00
Merge pull request #102 from das7pad/feat-image-digest
[taglist] display the image digest
This commit is contained in:
commit
2eeae54bca
11 changed files with 180 additions and 20 deletions
2
dist/scripts/docker-registry-ui-static.js
vendored
2
dist/scripts/docker-registry-ui-static.js
vendored
File diff suppressed because one or more lines are too long
2
dist/scripts/docker-registry-ui.js
vendored
2
dist/scripts/docker-registry-ui.js
vendored
File diff suppressed because one or more lines are too long
2
dist/style.css
vendored
2
dist/style.css
vendored
File diff suppressed because one or more lines are too long
|
@ -58,6 +58,7 @@
|
|||
<script src="tags/dialogs/menu.tag" type="riot/tag"></script>
|
||||
<script src="tags/image-size.tag" type="riot/tag"></script>
|
||||
<script src="tags/image-date.tag" type="riot/tag"></script>
|
||||
<script src="tags/image-content-digest.tag" type="riot/tag"></script>
|
||||
<script src="tags/pagination.tag" type="riot/tag"></script>
|
||||
<script src="tags/app.tag" type="riot/tag"></script>
|
||||
<script src="scripts/http.js"></script>
|
||||
|
|
|
@ -22,6 +22,30 @@ function Http() {
|
|||
this._headers = {};
|
||||
}
|
||||
|
||||
Http.prototype.getContentDigest = function(cb) {
|
||||
if (this.oReq.hasHeader('Docker-Content-Digest')) {
|
||||
// Same origin or advanced CORS headers set:
|
||||
// 'Access-Control-Expose-Headers: Docker-Content-Digest'
|
||||
cb(this.oReq.getResponseHeader('Docker-Content-Digest'))
|
||||
} else if (window.crypto && window.TextEncoder) {
|
||||
crypto.subtle.digest(
|
||||
'SHA-256',
|
||||
new TextEncoder().encode(this.oReq.responseText)
|
||||
).then(function (buffer) {
|
||||
cb(
|
||||
'sha256:' + Array.from(
|
||||
new Uint8Array(buffer)
|
||||
).map(function(byte) {
|
||||
return byte.toString(16).padStart(2, '0');
|
||||
}).join('')
|
||||
);
|
||||
})
|
||||
} else {
|
||||
// IE and old Edge
|
||||
// simply do not call the callback and skip the setup downstream
|
||||
}
|
||||
};
|
||||
|
||||
Http.prototype.addEventListener = function(e, f) {
|
||||
this._events[e] = f;
|
||||
const self = this;
|
||||
|
|
|
@ -49,7 +49,7 @@ main {
|
|||
}
|
||||
|
||||
material-card, pagination .conatianer {
|
||||
max-width: 75%;
|
||||
max-width: 95%;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
@ -66,10 +66,10 @@ pagination .conatianer .pagination-centered {
|
|||
margin: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 950px){
|
||||
material-card {
|
||||
width: 95%;
|
||||
max-width: 750px;
|
||||
/* 1515px * 0.95 = 1440px */
|
||||
@media screen and (min-width: 1515px){
|
||||
material-card, pagination .conatianer {
|
||||
max-width: 1440px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +105,7 @@ h2 {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.material-card-title-action h2 .source-hint,
|
||||
.material-card-title-action h2 .item-count {
|
||||
font-size: 0.7em;
|
||||
margin-left: 1em;
|
||||
|
@ -398,7 +399,26 @@ select {
|
|||
}
|
||||
|
||||
.copy-to-clipboard {
|
||||
padding: 12px 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
#image-tag-header {
|
||||
/* spacing for clipboard + default th spacing */
|
||||
/* 5 + 2 + 3 + 24 + 3 + 2 + 18 */
|
||||
padding-right: 57px;
|
||||
}
|
||||
image-tag, .copy-to-clipboard {
|
||||
display: inline-block;
|
||||
}
|
||||
image-content-digest {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 1024px) {
|
||||
#image-content-digest-header {
|
||||
padding-right: 57px;
|
||||
}
|
||||
image-content-digest {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.show-tag-history {
|
||||
|
|
|
@ -92,6 +92,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
registryUI.errorSnackbar = function(message) {
|
||||
return registryUI.snackbar(message, true);
|
||||
};
|
||||
registryUI.showErrorCanNotReadContentDigest = function() {
|
||||
registryUI.errorSnackbar(
|
||||
'Access on registry response was blocked. Try adding the header ' +
|
||||
'`Access-Control-Expose-Headers: Docker-Content-Digest`' +
|
||||
' to your proxy or registry: ' +
|
||||
'https://docs.docker.com/registry/configuration/#http'
|
||||
);
|
||||
};
|
||||
registryUI.cleanName = function() {
|
||||
const url = registryUI.pullUrl || (registryUI.url() && registryUI.url().length > 0 && registryUI.url()) || window.location.host;
|
||||
return registryUI.stripHttps(url);
|
||||
|
@ -113,6 +121,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
registryUI.DockerImage = function(name, tag) {
|
||||
this.name = name;
|
||||
this.tag = tag;
|
||||
this.chars = 0;
|
||||
riot.observable(this);
|
||||
this.on('get-size', function() {
|
||||
if (this.size !== undefined) {
|
||||
|
@ -132,6 +141,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
}
|
||||
return this.fillInfo();
|
||||
});
|
||||
this.on('content-digest-chars', function (chars) {
|
||||
this.chars = chars;
|
||||
});
|
||||
this.on('get-content-digest-chars', function() {
|
||||
return this.trigger('content-digest-chars', this.chars);
|
||||
});
|
||||
this.on('get-content-digest', function() {
|
||||
if (this.digest !== undefined) {
|
||||
return this.trigger('content-digest', this.digest);
|
||||
}
|
||||
return this.fillInfo();
|
||||
});
|
||||
};
|
||||
|
||||
registryUI.DockerImage._tagReduce = function(acc, e) {
|
||||
|
@ -178,6 +199,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
self.layers = response.layers;
|
||||
self.trigger('size', self.size);
|
||||
self.trigger('sha256', self.sha256);
|
||||
oReq.getContentDigest(function (digest) {
|
||||
self.digest = digest;
|
||||
self.trigger('content-digest', digest);
|
||||
});
|
||||
self.getBlobs(response.config.digest)
|
||||
} else if (this.status == 404) {
|
||||
registryUI.errorSnackbar('Manifest for ' + self.name + ':' + self.tag + ' not found');
|
||||
|
|
|
@ -15,13 +15,29 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<copy-to-clipboard>
|
||||
<div class="copy-to-clipboard">
|
||||
<input ref="input" style="display: none; width: 1px; height: 1px;" value="{ this.dockerCmd }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onclick="{ this.copy }" title="Copy pull command.">
|
||||
<i class="material-icons">content_copy</i>
|
||||
</material-button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
this.dockerCmd = 'docker pull ' + registryUI.cleanName() + '/' + opts.image.name + ':' + opts.image.tag;
|
||||
this.prefix = 'docker pull ' + registryUI.cleanName() + '/' + opts.image.name;
|
||||
const self = this;
|
||||
if (opts.target === 'tag') {
|
||||
self.dockerCmd = self.prefix + ':' + opts.image.tag;
|
||||
} else {
|
||||
opts.image.one('content-digest', function (digest) {
|
||||
self.dockerCmd = self.prefix + '@' + digest;
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
}
|
||||
|
||||
this.copy = function () {
|
||||
if (!self.dockerCmd) {
|
||||
registryUI.showErrorCanNotReadContentDigest();
|
||||
return;
|
||||
}
|
||||
const copyText = this.refs['input'];
|
||||
copyText.style.display = 'block';
|
||||
copyText.select();
|
||||
|
|
48
src/tags/image-content-digest.tag
Normal file
48
src/tags/image-content-digest.tag
Normal file
|
@ -0,0 +1,48 @@
|
|||
<!--
|
||||
Copyright (C) 2019 Jakob Ackermann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<image-content-digest>
|
||||
<div title="{ this.title }">{ this.display_id }</div>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
self.chars = -1;
|
||||
|
||||
self.onResize = function(chars) {
|
||||
if (chars === self.chars) {
|
||||
return;
|
||||
}
|
||||
self.chars = chars;
|
||||
if (chars >= 70) {
|
||||
self.display_id = self.digest;
|
||||
self.title = '';
|
||||
} else if (chars === 0) {
|
||||
self.display_id = '';
|
||||
self.title = self.digest;
|
||||
} else {
|
||||
self.display_id = self.digest.slice(0, chars) + '...';
|
||||
self.title = self.digest;
|
||||
}
|
||||
self.update();
|
||||
};
|
||||
|
||||
opts.image.one('content-digest', function(digest) {
|
||||
self.digest = digest;
|
||||
opts.image.on('content-digest-chars', self.onResize);
|
||||
opts.image.trigger('get-content-digest-chars');
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
</script>
|
||||
</image-content-digest>
|
|
@ -38,7 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
registryUI.taglist.go(name);
|
||||
if (this.status == 200) {
|
||||
if (!this.hasHeader('Docker-Content-Digest')) {
|
||||
registryUI.errorSnackbar('You need to add Access-Control-Expose-Headers: [\'Docker-Content-Digest\'] in your server configuration.');
|
||||
registryUI.showErrorCanNotReadContentDigest();
|
||||
return;
|
||||
}
|
||||
const digest = this.getResponseHeader('Docker-Content-Digest');
|
||||
|
|
|
@ -22,7 +22,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
Tags of { registryUI.name() + '/' + registryUI.taglist.name }
|
||||
Tags of { registryUI.taglist.name }
|
||||
<div class="source-hint">
|
||||
Sourced from { registryUI.name() + '/' + registryUI.taglist.name }
|
||||
</div>
|
||||
<div class="item-count">{ registryUI.taglist.tags.length } tags</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
@ -38,12 +41,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<table show="{ registryUI.taglist.loadend }" style="border: none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="material-card-th-left">Repository</th>
|
||||
<th></th>
|
||||
<th>Creation date</th>
|
||||
<th>Size</th>
|
||||
<th id="image-content-digest-header">Content Digest</th>
|
||||
|
||||
<th
|
||||
id="image-tag-header"
|
||||
class="{ registryUI.taglist.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
|
||||
onclick="registryUI.taglist.reverse();">Tag
|
||||
</th>
|
||||
|
@ -57,18 +60,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr each="{ image in this.opts.tags }">
|
||||
<td class="material-card-th-left">{ image.name }</td>
|
||||
<td class="copy-to-clipboard">
|
||||
<copy-to-clipboard image={ image }/>
|
||||
</td>
|
||||
<td>
|
||||
<image-date image="{ image }"/>
|
||||
</td>
|
||||
<td>
|
||||
<image-size image="{ image }"/>
|
||||
</td>
|
||||
<td>
|
||||
<image-content-digest image="{ image }"/>
|
||||
<copy-to-clipboard target="digest" image={ image }/>
|
||||
</td>
|
||||
<td>
|
||||
<image-tag image="{ image }"/>
|
||||
<copy-to-clipboard target="tag" image={ image }/>
|
||||
</td>
|
||||
<td class="show-tag-history">
|
||||
<tag-history-button image={ image }/>
|
||||
|
@ -84,6 +88,27 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script>
|
||||
var self = registryUI.taglist.instance = this;
|
||||
self.page = registryUI.getPageQueryParam();
|
||||
registryUI.taglist.tags = [];
|
||||
const onResize = function() {
|
||||
// window.innerWidth is a blocking access, cache its result.
|
||||
const innerWidth = window.innerWidth;
|
||||
var chars = 0;
|
||||
if (innerWidth >= 1440) {
|
||||
chars = 71;
|
||||
} else if (innerWidth < 1024) {
|
||||
chars = 0;
|
||||
} else {
|
||||
// SHA256:12345678 + scaled between 1024 and 1440px
|
||||
chars = 15 + 56 * ((innerWidth - 1024) / 416);
|
||||
}
|
||||
registryUI.taglist.tags.map(function (image) {
|
||||
image.trigger('content-digest-chars', chars);
|
||||
});
|
||||
};
|
||||
window.addEventListener('resize', onResize);
|
||||
// this may be run before the final document size is available, so schedule
|
||||
// a correction once everything is set up.
|
||||
window.requestAnimationFrame(onResize);
|
||||
|
||||
this.multiDelete = false;
|
||||
this.toDelete = 0;
|
||||
|
@ -167,10 +192,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
oReq.addEventListener('load', function() {
|
||||
registryUI.taglist.tags = [];
|
||||
if (this.status == 200) {
|
||||
registryUI.taglist.tags = JSON.parse(this.responseText).tags || [];
|
||||
registryUI.taglist.tags = registryUI.taglist.tags.map(function(tag) {
|
||||
const tags = JSON.parse(this.responseText).tags || [];
|
||||
registryUI.taglist.tags = tags.map(function(tag) {
|
||||
return new registryUI.DockerImage(registryUI.taglist.name, tag);
|
||||
}).sort(registryUI.DockerImage.compare);
|
||||
window.requestAnimationFrame(onResize);
|
||||
self.trigger('page-update', Math.min(self.page, registryUI.getNumPages(registryUI.taglist.tags)))
|
||||
} else if (this.status == 404) {
|
||||
registryUI.snackbar('Server not found', true);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue