Merge pull request #43 from Joxit/feat/copy-to-clipboard

[feat #42] Add copy to clipboard for images with tag and add sha256
This commit is contained in:
Jones Magloire 2018-06-18 22:33:49 +02:00 committed by GitHub
commit b78fd358d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 177 additions and 45 deletions

View file

@ -25,6 +25,8 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- Display image size (see #30)
- Add Title when using REGISTRY_URL (see #28)
- Alpine and Debian based images with supports for arm32v7
- Copy `docker pull` command to clipbloard
- Show sha256 for specific tag (hover image tag)
## Getting Started

View file

@ -49,7 +49,7 @@ gulp.task('riot-tag', ['html'], function() {
});
gulp.task('riot-static-tag', ['html'], function() {
return gulp.src(['src/tags/catalog.tag', 'src/tags/app.tag', 'src/tags/taglist.tag', 'src/tags/remove-image.tag', 'src/tags/image-size.tag'])
return gulp.src(['src/tags/catalog.tag', 'src/tags/app.tag', 'src/tags/taglist.tag', 'src/tags/copy-to-clipboard.tag', 'src/tags/remove-image.tag', 'src/tags/image-size.tag', 'src/tags/image-tag.tag'])
.pipe(concat('tags-static.js'))
.pipe(riot())
.pipe(minifier({}, uglify))

View file

@ -15,7 +15,7 @@
"devDependencies": {
"del": "^3.0.0",
"gulp": "^3.9",
"gulp-clean-css": "^3.9.3",
"gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.0",
"gulp-filter": "^5.1.0",
"gulp-htmlmin": "^3.0.0",
@ -27,7 +27,7 @@
"riot": "^3.10.0",
"riot-mui": "^0.1.1",
"riot-route": "^3.1.3",
"uglify-js": "^3.3.16",
"uglify-js": "^3.4.0",
"uglify-js-harmony": "^2.7.7"
}
}

View file

@ -40,7 +40,9 @@
<!-- build:js scripts/tags.js -->
<script src="tags/catalog.tag" type="riot/tag"></script>
<script src="tags/taglist.tag" type="riot/tag"></script>
<script src="tags/image-tag.tag" type="riot/tag"></script>
<script src="tags/remove-image.tag" type="riot/tag"></script>
<script src="tags/copy-to-clipboard.tag" type="riot/tag"></script>
<script src="tags/add.tag" type="riot/tag"></script>
<script src="tags/change.tag" type="riot/tag"></script>
<script src="tags/remove.tag" type="riot/tag"></script>

View file

@ -15,9 +15,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
html > body {
html>body {
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif !important;
}
html, body {
margin: 0;
height: 100%;
@ -64,9 +65,11 @@ material-spinner {
display: flex;
flex-direction: column;
}
material-navbar {
height: 64px;
}
.logo {
padding: 0 16px 0 72px;
text-decoration: none;
@ -75,6 +78,7 @@ material-navbar {
letter-spacing: .02em;
font-weight: 400;
}
h2 {
padding: 16px;
margin: auto;
@ -83,15 +87,18 @@ h2 {
line-height: normal;
overflow: hidden;
}
.list {
display: block;
padding: 8px 0;
list-style: none;
}
.list.highlight>li:hover {
background-color: #eee;
cursor: pointer;
}
.list>li {
box-sizing: border-box;
line-height: 1;
@ -99,6 +106,7 @@ h2 {
padding: 0 16px;
overflow: hidden;
}
.list>li i.material-icons {
margin-right: 32px;
height: 24px;
@ -107,6 +115,7 @@ h2 {
box-sizing: border-box;
color: #757575;
}
.list>li>span {
height: 100%;
text-decoration: none;
@ -138,7 +147,7 @@ material-card table {
width: 100%;
border: none;
position: relative;
border: 1px solid rgba(0,0,0,.12);
border: 1px solid rgba(0, 0, 0, .12);
border-collapse: collapse;
white-space: nowrap;
font-size: 13px;
@ -151,7 +160,7 @@ material-card table th {
vertical-align: bottom;
line-height: 24px;
height: 48px;
color: rgba(0,0,0,.54);
color: rgba(0, 0, 0, .54);
box-sizing: border-box;
padding: 0 18px 12px 18px;
text-align: right;
@ -169,7 +178,7 @@ material-card table tbody tr {
position: relative;
height: 48px;
transition-duration: .28s;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
transition-property: background-color;
}
@ -177,24 +186,23 @@ material-card table td {
font-size: 16px;
position: relative;
height: 48px;
border-top: 1px solid rgba(0,0,0,.12);
border-bottom: 1px solid rgba(0,0,0,.12);
border-top: 1px solid rgba(0, 0, 0, .12);
border-bottom: 1px solid rgba(0, 0, 0, .12);
padding: 12px 18px;
box-sizing: border-box;
vertical-align: middle;
text-align: right;
}
material-card table th.material-card-th-sorted-ascending:hover,
material-card table th.material-card-th-sorted-descending:hover {
material-card table th.material-card-th-sorted-ascending:hover, material-card table th.material-card-th-sorted-descending:hover {
cursor: pointer;
}
material-card table th.material-card-th-sorted-ascending:hover:before,
material-card table th.material-card-th-sorted-descending:hover:before {
color: rgba(0,0,0,.26);
material-card table th.material-card-th-sorted-ascending:hover:before, material-card table th.material-card-th-sorted-descending:hover:before {
color: rgba(0, 0, 0, .26);
}
material-card table th.material-card-th-sorted-ascending:before,
material-card table th.material-card-th-sorted-descending:before {
material-card table th.material-card-th-sorted-ascending:before, material-card table th.material-card-th-sorted-descending:before {
font-family: 'Material Icons';
font-weight: 400;
font-style: normal;
@ -204,6 +212,7 @@ material-card table th.material-card-th-sorted-descending:before {
margin-right: 5px;
vertical-align: sub;
}
material-card table th.material-card-th-sorted-descending:before {
content: "\e5db";
}
@ -233,7 +242,7 @@ menu .overlay {
}
#menu-control-button {
background: rgba(255,255,255,0);
background: rgba(255, 255, 255, 0);
float: right;
}
@ -275,7 +284,7 @@ material-popup material-button {
}
material-popup material-button:hover material-waves {
background-color: hsla(0,0%,75%,.2);
background-color: hsla(0, 0%, 75%, .2);
}
material-popup .popup {
@ -307,4 +316,12 @@ select {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
}
.copy-to-clipboard {
padding: 12px 5px;
}
.copy-to-clipboard a:hover {
cursor: pointer;
}

View file

@ -77,6 +77,13 @@
registryUI.errorSnackbar = function (message) {
return registryUI.snackbar(message, true);
}
registryUI.cleanName = function() {
var url = registryUI.url() || registryUI.name();
if (url) {
return url.startsWith('http') ? url.replace(/https?:\/\//, '') : url;
}
return '';
}
route.parser(null, function(path, filter) {
const f = filter
.replace(/\?/g, '\\?')
@ -86,6 +93,55 @@
const args = path.match(re)
if (args) return args.slice(1)
});
registryUI.DockerImage = function (name, tag) {
this.name = name;
this.tag = tag;
riot.observable(this);
this.on('get-size', function() {
if (this.size !== undefined) {
return this.trigger('size', this.size);
}
return this.fillInfo();
});
this.on('get-sha256', function() {
if (this.size !== undefined) {
return this.trigger('sha256', this.sha256);
}
return this.fillInfo();
});
};
registryUI.DockerImage.compare = function(e1, e2) {
return e1.tag.localeCompare(e2);
};
registryUI.DockerImage.prototype.fillInfo = function() {
if (this._fillInfoWaiting) {
return;
}
this._fillInfoWaiting = true;
var oReq = new Http();
var self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
var response = JSON.parse(this.responseText);
self.size = response.layers.reduce(function (acc, e) {
return acc + e.size;
}, 0);
self.sha256 = response.config.digest;
self.trigger('size', self.size);
self.trigger('sha256', self.sha256);
} else if (this.status == 404) {
registryUI.errorSnackbar('Manifest for ' + self.name + ':' + self.tag + ' not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('GET', registryUI.url() + '/v2/' + self.name + '/manifests/' + self.tag);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
}
route.start(true);
</script>
</app>

View file

@ -0,0 +1,34 @@
<!--
Copyright (C) 2016-2018 Jones Magloire @Joxit
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/>.
-->
<copy-to-clipboard>
<input ref="input" style="display: none; width: 1px; height: 1px;" value="{ this.dockerCmd }">
<a onclick="{ this.copy }" title="Copy pull command.">
<i class="material-icons">content_copy</i>
</a>
<script type="text/javascript">
this.dockerCmd = 'docker pull ' + registryUI.cleanName() + '/' + opts.image.name + ':' + opts.image.tag;
this.copy = function () {
var copyText = this.refs['input'];
copyText.style.display = 'block';
copyText.select();
document.execCommand('copy');
copyText.style.display = 'none';
registryUI.snackbar('`' + this.dockerCmd + '` has been copied to clipbloard.')
};
</script>
</copy-to-clipboard>

View file

@ -15,7 +15,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<image-size>
<div>{ this.bytesToSize(this.size) }</div>
<div title="Compressed size of your image.">{ this.bytesToSize(this.size) }</div>
<script type="text/javascript">
var self = this;
this.bytesToSize = function (bytes) {
@ -28,21 +28,10 @@
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.ceil(bytes / Math.pow(1024, i)) + ' ' + sizes[i];
};
var oReq = new Http();
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
self.size = JSON.parse(this.responseText).layers.reduce(function (acc, e) {
return acc + e.size;
}, 0);
self.update();
} else if (this.status == 404) {
registryUI.errorSnackbar('Manifest for ' + opts.name + ':' + opts.tag + ' not found');
} else {
registryUI.snackbar(this.responseText);
}
opts.image.on('size', function(size) {
self.size = size;
self.update();
});
oReq.open('GET', registryUI.url() + '/v2/' + opts.name + '/manifests/' + opts.tag);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
opts.image.trigger('get-size');
</script>
</image-size>

27
src/tags/image-tag.tag Normal file
View file

@ -0,0 +1,27 @@
<!--
Copyright (C) 2016-2018 Jones Magloire @Joxit
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-tag>
<div title="{ this.sha256 }">{ opts.image.tag }</div>
<script type="text/javascript">
var self = this;
opts.image.on('sha256', function(sha256) {
self.sha256 = sha256.substring(0, 19);
self.update();
});
opts.image.trigger('get-sha256');
</script>
</image-tag>

View file

@ -15,13 +15,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<remove-image>
<a href="#" onclick="registryUI.removeImage.remove('{ opts.name }', '{ opts.tag }')">
<a href="#" title="This will delete the image." onclick="registryUI.removeImage.remove('{ opts.image.name }', '{ opts.image.tag }')">
<i class="material-icons">delete</i>
</a>
<script type="text/javascript">
registryUI.removeImage = registryUI.removeImage || {}
registryUI.removeImage.update = this.update;
registryUI.removeImage = registryUI.removeImage || {};
registryUI.removeImage.remove = function (name, tag) {
var oReq = new Http();
oReq.addEventListener('loadend', function () {

View file

@ -30,18 +30,22 @@
<thead>
<tr>
<th class="material-card-th-left">Repository</th>
<th></th>
<th>Size</th>
<th class="{ registryUI.taglist.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }" onclick="registryUI.taglist.reverse();">Tag</th>
<th show="{ registryUI.isImageRemoveActivated }"></th>
</tr>
</thead>
<tbody>
<tr each="{ item in registryUI.taglist.tags }">
<td class="material-card-th-left">{ registryUI.taglist.name }</td>
<td><image-size name={ registryUI.taglist.name } tag={ item } /></td>
<td>{ item }</td>
<tr each="{ image in registryUI.taglist.tags }">
<td class="material-card-th-left">{ image.name }</td>
<td class="copy-to-clipboard">
<copy-to-clipboard image={ image }/>
</td>
<td><image-size image="{ image }" /></td>
<td><image-tag image="{ image }" /></td>
<td show="{ registryUI.isImageRemoveActivated }">
<remove-image name={ registryUI.taglist.name } tag={ item }/>
<remove-image image={ image }/>
</td>
</tr>
</tbody>
@ -58,7 +62,9 @@
registryUI.taglist.tags = [];
if (this.status == 200) {
registryUI.taglist.tags = JSON.parse(this.responseText).tags || [];
registryUI.taglist.tags.sort();
registryUI.taglist.tags = registryUI.taglist.tags.map(function(tag) {
return new registryUI.DockerImage(registryUI.taglist.name, tag);
}).sort(registryUI.DockerImage.compare);
} else if (this.status == 404) {
registryUI.snackbar('Server not found', true);
} else {
@ -86,7 +92,7 @@
registryUI.taglist.tags.reverse();
registryUI.taglist.asc = false;
} else {
registryUI.taglist.tags.sort();
registryUI.taglist.tags.sort(registryUI.DockerImage.compare);
registryUI.taglist.asc = true;
}
registryUI.taglist.instance.update();