Compare commits

..

19 Commits
1.2.2 ... 1.3.0

Author SHA1 Message Date
Joxit
a0c88d06f0 Release v1.3.0: pagination in taglist and indent aggregated entries 2019-07-18 01:13:57 +02:00
Joxit
eec736d4e8 feat: Indent aggregated entries after expansion
closes: #90
2019-07-17 23:59:59 +02:00
Joxit
9e99b08b82 fix: delete toggle button when delete is disabled 2019-07-06 11:59:59 +02:00
Jones Magloire
0f805daafa Merge pull request #86 from Joxit/feat/36-pagination
[Feat #36] Pagination in taglist

## UI update

![image](https://user-images.githubusercontent.com/5153882/59188702-8da13700-8b78-11e9-87ff-64d1b6c8d380.png)

resolves #36 

cc @madhukar93 @gionn
2019-07-05 00:11:23 +02:00
Joxit
8bd1f31c9c Release v1.2.3: Custom header supports 2019-07-04 23:09:11 +02:00
Jones Magloire
7777ff28df Merge pull request #89 from Joxit/custom-headers
Supports custom headers when the ui is used as proxy

## Background

Headers can be useful in some cases such as avoid sending credentials when you are on the UI (like #87). Or give to the registry server other properties such as `X-Forward-For` or `Server` headers for monitoring.

## How to use ?

This is compatible only with static version of the UI and used with `REGISTRY_URL` variable.
When you want to add a custom header, add to the registry ui a environment variable or entry in `/etc/nginx/.env` which looks like `NGINX_PROXY_HEADER_Custom_Header`. All underscores (`_`) will be replaced by hyphens (`-`). 

Some example of custom headers as variable:
- `NGINX_PROXY_HEADER_Authorization` for Basic auth credentials
- `NGINX_PROXY_HEADER_X_Forwarded_For` for identifying the originating IP address of a client

An example is bundled with this PR

closes: #87
2019-07-03 00:09:18 +02:00
Joxit
4fee7b44d3 feat: Supports custom headers via file /etc/nginx/.env
Remove the print of headers for security
2019-07-02 23:04:32 +02:00
Joxit
79960ea52d fix(pagination): page switch and regression on multi delete 2019-06-27 23:54:20 +02:00
Joxit
7716f8b44a feat: Supports custom headers when the ui is used as proxy 2019-06-24 23:54:21 +02:00
Joxit
0ac7a151d9 fix(pagination): Wrong calcul for num pages 2019-06-23 22:55:32 +02:00
Joxit
ef149bf1cc chore: Memory optimization when delete is not activated 2019-06-17 00:46:46 +02:00
Joxit
e5a406a6ba fix(pagination): Reset the number of selected tags to delete when the page is updated 2019-06-16 23:07:54 +02:00
Joxit
dbb746981a fix(pagination): Improve spacing for page next and first page buttons 2019-06-15 22:37:24 +02:00
Joxit
92fc37adb4 feat(pagination): Add handler to pagination buttons 2019-06-04 22:06:14 +02:00
Joxit
02210e0943 fix(pagination): getPage doesn't work for page 2 and now support query param 2019-06-03 01:39:23 +02:00
Joxit
0199f87087 feat(pagination): Identify the current page 2019-05-31 00:44:29 +02:00
Joxit
3399030e4e feat(pagination): Add pagination component with its style 2019-05-30 00:11:05 +02:00
Joxit
660a938d6e feat(pagination): Small UI improvements 2019-05-27 22:23:24 +02:00
Joxit
7356591292 feat(pagination): Show only 100 first elements 2019-05-26 01:20:02 +02:00
27 changed files with 391 additions and 87 deletions

View File

@@ -20,7 +20,7 @@ In the **static interface**, it will connect to a single registry and will not c
This web user interface uses [Riot](https://github.com/Riot/riot) the react-like user interface micro-library and [riot-mui](https://github.com/kysonic/riot-mui) components.
## [Project Page](https://joxit.dev/docker-registry-ui) and [Live Demo](https://joxit.dev/docker-registry-ui/demo/)
## [Project Page](https://joxit.dev/docker-registry-ui), [Live Demo](https://joxit.dev/docker-registry-ui/demo/) [Examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples)
![preview](https://raw.github.com/Joxit/docker-registry-ui/master/docker-registry-ui.gif "Preview of Docker Registry UI")
@@ -45,6 +45,7 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- Use `joxit/docker-registry-ui:static` as reverse proxy (with `REGISTRY_URL` environment variable) to your docker registry (This will avoid CORS) **static interface**.
- Add Title when using `REGISTRY_URL` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)) **static interface**.
- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)) **static interface**.
- Add custom header via environment variable and file (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) **static interface**
## Getting Started
@@ -221,3 +222,4 @@ auth:
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))

View File

@@ -1,5 +1,5 @@
#!/bin/sh
$@
sed -i "s,\${URL},${URL}," scripts/docker-registry-ui.js
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," scripts/docker-registry-ui.js
sed -i "s,\${PULL_URL},${PULL_URL}," scripts/docker-registry-ui.js
@@ -8,13 +8,31 @@ if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
fi
get_nginx_proxy_headers() {
(
env &&
if [ -f "/etc/nginx/.env" ]; then
cat /etc/nginx/.env
# Force new line
echo ""
fi
) | while read e; do
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_HEADER_')" ]; then
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_HEADER_//' | sed 's/_/-/g')
value=${e#*=}
echo -n "proxy_set_header ${key} \"${value}\"; "
fi
done
}
if [ -n "${REGISTRY_URL}" ] ; then
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," /etc/nginx/conf.d/default.conf
sed -i "s^\${NGINX_PROXY_HEADERS}^$(get_nginx_proxy_headers)^" /etc/nginx/conf.d/default.conf
sed -i "s,#!,," /etc/nginx/conf.d/default.conf
fi
if [ -z "$@" ]; then
nginx -g "daemon off;"
exec nginx -g "daemon off;"
else
$@
exec $@
fi

13
bin/fill-registry Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
for i in alpine chronograf:alpine consul debian jawg/mapnik3 nginx:alpine postgres:alpine redis:alpine telegraf:alpine joxit/docker-registry-ui joxit/kosmtik joxit/node joxit/rust-openssl joxit/tile-server-ui; do
docker pull $i
docker tag $i 127.0.0.1:5000/$i
docker push 127.0.0.1:5000/$i
done
for i in arm32v7-static 1.2-debian-static master-static 1.2 arm64v8 arm32v7 arm64v8-static master 1.2-debian latest static debian-static debian 1.2-static 1.1 1.1-static 1.1-debian-static 1.1-debian ; do
docker pull joxit/docker-registry-ui:$i
docker tag joxit/docker-registry-ui:$i 127.0.0.1:5000/joxit/docker-registry-ui:$i
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$i
done

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/style.css vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 764 KiB

After

Width:  |  Height:  |  Size: 686 KiB

10
examples/README.md Normal file
View File

@@ -0,0 +1,10 @@
## Examples
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone)
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/master/examples/traefik)
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/88))
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))

View File

@@ -0,0 +1,19 @@
# Set custom headers to the registry example
The interface and the docker registry will be accessible with <http://localhost>.
This example highlight the usage of custom headers when the UI is used as a proxy. When you wants to use a header name with hyphens, replace them by underscores in the variable. You can put headers in environment variable or in config file `/etc/nginx/.env`. They have the same writing style.
Headers can be useful in some cases such as avoid sending credentials when you are on the UI. Or give to the registry server other properties such as X-Forward-For header.
I will set these two headers in this example. X-Forward-For by environment variable and Authorization by file.
In order to set your credentials in the header, you need to know how [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) header works. Here we use the `Basic` authentication scheme, the credentials are constructed like this:
- The username and the password are combined with a colon (`registry:ui`).
- The resulting string is base64 encoded (`cmVnaXN0cnk6dWk=`). You can simply run `echo -n "registry:ui" | base64`.
- In your header, put this value `Basic cmVnaXN0cnk6dWk=`
- In your `/etc/nginx/.env`, the file will contains `NGINX_PROXY_HEADER_Authorization=Basic cmVnaXN0cnk6dWk=`
For X-Forward-For, replace all hyphens by underscores, and the value will be a nginx variable which is `$proxy_add_x_forwarded_for`. In your docker compose you will need to duplicate the `$` character. In your docker-compose, your environment will look like `NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for`
As usual, run the project with `docker-compose up -d` (for background mode)

View File

@@ -0,0 +1,28 @@
version: '2.0'
services:
registry:
image: registry:2.7
volumes:
- ./registry-data:/var/lib/registry
- ./registry-config/credentials.yml:/etc/docker/registry/config.yml
- ./registry-config/htpasswd:/etc/docker/registry/htpasswd
networks:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
- NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for
volumes:
- ./nginx.env:/etc/nginx/.env
depends_on:
- registry
networks:
- registry-ui-net
networks:
registry-ui-net:

View File

@@ -0,0 +1 @@
NGINX_PROXY_HEADER_Authorization=Basic cmVnaXN0cnk6dWk=

View File

@@ -0,0 +1,25 @@
version: 0.1
log:
fields:
service: registry
storage:
delete:
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['http://localhost']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']
auth:
htpasswd:
realm: basic-realm
path: /etc/docker/registry/htpasswd

View File

@@ -0,0 +1 @@
registry:$2y$11$1bmuJLK8HrQl5ACS/WeqRuJLUArUZfUcP2R23asmozEpfN76.pCHy

View File

@@ -25,6 +25,7 @@ server {
#! return 404;
#! }
#! proxy_set_header Host $http_host;
#! ${NGINX_PROXY_HEADERS}
#! proxy_pass ${REGISTRY_URL};
#! }

View File

@@ -1,6 +1,6 @@
{
"name": "docker-registry-ui",
"version": "1.2.2",
"version": "1.3.0",
"scripts": {
"build": "./node_modules/gulp/bin/gulp.js build"
},
@@ -14,7 +14,7 @@
"dependencies": {},
"devDependencies": {
"del": "^3.0.0",
"gulp": "^4.0.1",
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-concat": "^2.6.0",
"gulp-filter": "^5.1.0",

View File

@@ -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/pagination.tag" type="riot/tag"></script>
<script src="tags/app.tag" type="riot/tag"></script>
<script src="scripts/http.js"></script>
<script src="scripts/script.js"></script>

View File

@@ -85,7 +85,7 @@ registryUI.removeServer = function(url) {
}
registryUI.updateHistory = function(url) {
history.pushState(null, '', (url ? '?url=' + registryUI.encodeURI(url) : '?') + window.location.hash);
registryUI.updateQueryString({ url: registryUI.encodeURI(url) })
registryUI._url = url;
}
@@ -100,10 +100,12 @@ registryUI.getUrlQueryParam = function () {
};
registryUI.encodeURI = function(url) {
if (!url) { return; }
return url.indexOf('&') < 0 ? window.encodeURIComponent(url) : btoa(url);
};
registryUI.decodeURI = function(url) {
if (!url) { return; }
return url.startsWith('http') ? window.decodeURIComponent(url) : atob(url);
};

View File

@@ -65,4 +65,50 @@ registryUI.getHistoryIcon = function(attribute) {
default:
''
}
}
registryUI.getPage = function(elts, page, limit) {
if (!limit) { limit = 100; }
if (!elts) { return []; }
return elts.slice((page - 1) * limit, limit * page);
}
registryUI.getNumPages = function(elts, limit) {
if (!limit) { limit = 100; }
if (!elts) { return 0; }
return Math.trunc(elts.length / limit) + 1;
}
registryUI.getPageLabels = function(page, nPages) {
var pageLabels = [];
var maxItems = 10;
if (nPages === 1) { return pageLabels; }
if (page !== 1 && nPages >= maxItems) {
pageLabels.push({'icon': 'first_page', page: 1});
pageLabels.push({'icon': 'chevron_left', page: page - 1});
}
var start = Math.round(Math.max(1, Math.min(page - maxItems / 2, nPages - maxItems + 1)));
for (var i = start; i < Math.min(nPages + 1, start + maxItems); i++) {
pageLabels.push({
page: i,
current: i === page,
'space-left': page === 1 && nPages > maxItems,
'space-right': page === nPages && nPages > maxItems
});
}
if (page !== nPages && nPages >= maxItems) {
pageLabels.push({'icon': 'chevron_right', page: page + 1});
pageLabels.push({'icon': 'last_page', page: nPages});
}
return pageLabels;
}
registryUI.updateQueryString = function(qs) {
var search = '';
for (var key in qs) {
if (qs[key] !== undefined) {
search += (search.length > 0 ? '&' : '?') +key + '=' + qs[key];
}
}
history.pushState(null, '', search + window.location.hash);
}

View File

@@ -48,14 +48,24 @@ main {
font-weight: inherit;
}
material-card {
min-height: 200px;
material-card, pagination .conatianer {
max-width: 75%;
margin: auto;
margin-top: 20px;
margin-bottom: 20px;
}
pagination .conatianer {
display: flex;
display: -moz-flex;
display: -webkit-flex;
display: -ms-flexbox;
}
pagination .conatianer .pagination-centered {
margin: auto;
}
@media screen and (max-width: 950px){
material-card {
width: 95%;
@@ -212,12 +222,14 @@ material-card table th {
}
material-card material-button:hover,
material-card table tbody tr:hover {
material-card table tbody tr:hover,
pagination material-button:hover {
background-color: #eee;
}
material-card material-button,
material-card table tbody tr {
material-card table tbody tr,
pagination material-button {
transition-duration: .28s;
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
transition-property: background-color;
@@ -326,7 +338,8 @@ dropdown-item, #menu-control-dropdown p {
background-color: #e0e0e0;
}
material-popup material-button {
material-popup material-button,
pagination material-button {
background-color: #fff;
color: #000;
}
@@ -445,7 +458,8 @@ tag-history-button button {
border: none;
}
material-card material-button {
material-card material-button,
pagination material-button {
max-height: 30px;
max-width: 30px;
}
@@ -454,7 +468,8 @@ material-button:hover material-waves {
background: none;
}
material-card material-button {
material-card material-button,
pagination material-button {
background-color: inherit;
}
@@ -475,6 +490,10 @@ catalog-element catalog-element.hide material-card {
opacity: 0;
}
catalog-element catalog-element .list > span i.material-icons {
margin-right: 48px;
}
remove-image {
width: 30px;
}
@@ -506,4 +525,30 @@ material-checkbox .checkbox {
material-checkbox .checkbox.checked {
background-color: #777;
}
pagination material-button {
padding: 0.2em 0.75em;
}
pagination material-button .content {
display: flex;
align-content: center;
line-height: 1.9em;
}
pagination material-button.current {
border: 1px solid rgba(0, 0, 0, .12);
}
pagination material-button.current.space-left {
margin-left: 85px;
}
pagination material-button.current.space-right {
margin-right: 85px;
}
pagination material-button .content i.material-icons {
color: #000;
}

View File

@@ -130,8 +130,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return this.fillInfo();
});
this.on('get-date', function() {
if (this.date !== undefined) {
return this.trigger('date', this.date);
if (this.creationDate !== undefined) {
return this.trigger('creation-date', this.creationDate);
}
return this.fillInfo();
});
@@ -225,6 +225,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
route('taglist/' + image);
};
registryUI.getPageQueryParam = function() {
var qs = route.query();
try {
return qs.page !== undefined ? parseInt(qs.page.replace(/#.*/, '')) : 1;
} catch(e) { return 1; }
}
registryUI.getQueryParams = function(update) {
var qs = route.query();
update = update || {};
for (var key in qs) {
if (qs[key] !== undefined) {
qs[key] = qs[key].replace(/#!.*/, '');
} else {
delete qs[key];
}
}
for (var key in update) {
if (update[key] !== undefined) {
qs[key] = update[key];
}
}
return qs;
}
route.start(true);
</script>
</app>

View File

@@ -21,13 +21,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<span>
<i class="material-icons">send</i>
{ typeof opts.item === "string" ? opts.item : opts.item.repo }
<div hide="{typeof opts.item === "string"}" class="item-count right">
<div if="{typeof opts.item !== "string"}" class="item-count right">
{ opts.item.images && opts.item.images.length } images
<i class="material-icons animated {expanded: opts.expanded}">expand_more</i>
</div>
</span>
</material-card>
<catalog-element hide="{typeof opts.item === "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
<catalog-element if="{typeof opts.item !== "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
<script>
this.on('mount', function() {
const self = this;

View File

@@ -16,17 +16,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<catalog>
<!-- Begin of tag -->
<material-card ref="catalog-tag" class="catalog">
<material-card ref="catalog-tag" class="catalog header">
<div class="material-card-title-action">
<h2>
Repositories of { registryUI.name() }
<div class="item-count">{ registryUI.catalog.length } images</div>
</h2>
</div>
<div hide="{ registryUI.catalog.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
</material-card>
<div hide="{ registryUI.catalog.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
<catalog-element each="{ item in registryUI.catalog.repositories }" />
<script>
registryUI.catalog.instance = this;

View File

@@ -24,5 +24,7 @@
self.localDate = date.toLocaleString()
self.update();
});
opts.image.trigger('get-date');
</script>
</image-date>

39
src/tags/pagination.tag Normal file
View File

@@ -0,0 +1,39 @@
<!--
Copyright (C) 2016-2019 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/>.
-->
<pagination>
<!-- Begin of tag -->
<div class="conatianer">
<div class="pagination-centered">
<material-button waves-color="rgba(158,158,158,.4)" each="{p in this.opts.pages}" class="{ current: p.current, space-left: p['space-left'], space-right: p['space-right']}">
<i show="{ p.icon }" class="material-icons">{ p.icon }</i>
<div hide="{ p.icon }">{ p.page }</div>
</material-button>
</div>
<div>
<script>
this.on('updated', function() {
if (!this.tags['material-button']) { return; }
var buttons = Array.isArray(this.tags['material-button']) ? this.tags['material-button'] : [this.tags['material-button']];
buttons.forEach(function(button) {
button.root.onclick = function() {
registryUI.taglist.instance.trigger('page-update', button.p.page)
}
});
});
</script>
<!-- End of tag -->
</pagination>

View File

@@ -15,63 +15,71 @@ 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/>.
-->
<remove-image>
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." hide="{ opts.multiDelete }">
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." if="{ !opts.multiDelete }">
<i class="material-icons">delete</i>
</material-button>
<material-checkbox show="{ opts.multiDelete }" title="Select this tag to delete it."></material-checkbox>
<material-checkbox if="{ opts.multiDelete }" title="Select this tag to delete it."></material-checkbox>
<script type="text/javascript">
const self = this;
this.on('update', function() {
if (!this.opts.multiDelete && this.tags['material-checkbox'].checked) {
this.tags['material-checkbox'].toggle();
}
this.on('updated', function() {
});
this.on('mount', function() {
this.delete = this.tags['material-button'].root.onclick = function(ignoreError) {
const name = self.opts.image.name;
const tag = self.opts.image.tag;
const oReq = new Http();
oReq.addEventListener('loadend', function() {
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.');
return;
}
const digest = this.getResponseHeader('Docker-Content-Digest');
const oReq = new Http();
oReq.addEventListener('loadend', function() {
if (this.status == 200 || this.status == 202) {
registryUI.taglist.display()
registryUI.snackbar('Deleting ' + name + ':' + tag + ' image. Run `registry garbage-collect config.yml` on your registry');
} else if (this.status == 404) {
ignoreError || registryUI.errorSnackbar('Digest not found');
} else {
registryUI.snackbar(this.responseText);
this.on('updated', function() {
if (self.multiDelete == self.opts.multiDelete) {
return;
}
if (this.tags['material-button']) {
this.delete = this.tags['material-button'].root.onclick = function(ignoreError) {
const name = self.opts.image.name;
const tag = self.opts.image.tag;
const oReq = new Http();
oReq.addEventListener('loadend', function() {
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.');
return;
}
});
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + digest);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.addEventListener('error', function() {
registryUI.errorSnackbar('An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].');
});
oReq.send();
} else if (this.status == 404) {
registryUI.errorSnackbar('Manifest for ' + name + ':' + tag + ' not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('HEAD', registryUI.url() + '/v2/' + name + '/manifests/' + tag);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
};
const digest = this.getResponseHeader('Docker-Content-Digest');
const oReq = new Http();
oReq.addEventListener('loadend', function() {
if (this.status == 200 || this.status == 202) {
registryUI.taglist.display()
registryUI.snackbar('Deleting ' + name + ':' + tag + ' image. Run `registry garbage-collect config.yml` on your registry');
} else if (this.status == 404) {
ignoreError || registryUI.errorSnackbar('Digest not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + digest);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.addEventListener('error', function() {
registryUI.errorSnackbar('An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].');
});
oReq.send();
} else if (this.status == 404) {
registryUI.errorSnackbar('Manifest for ' + name + ':' + tag + ' not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('HEAD', registryUI.url() + '/v2/' + name + '/manifests/' + tag);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
};
}
this.tags['material-checkbox'].on('toggle', function() {
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
});
if (this.tags['material-checkbox']) {
if (!this.opts.multiDelete && this.tags['material-checkbox'].checked) {
this.tags['material-checkbox'].toggle();
}
this.tags['material-checkbox'].on('toggle', function() {
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
});
}
self.multiDelete = self.opts.multiDelete;
});
</script>
</remove-image>

View File

@@ -15,7 +15,7 @@ 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/>.
-->
<tag-history>
<material-card ref="tag-history-tag" class="tag-history">
<material-card ref="tag-history-tag" class="tag-history header">
<div class="material-card-title-action">
<material-button waves-center="true" rounded="true" waves-color="#ddd">
<i class="material-icons">arrow_back</i>

View File

@@ -16,8 +16,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<taglist>
<!-- Begin of tag -->
<material-card ref="taglist-tag" class="taglist" multi-delete={ this.multiDelete }>
<div class="material-card-title-action">
<material-card class="header">
<div class="material-card-title-action ">
<material-button waves-center="true" rounded="true" waves-color="#ddd" onclick="registryUI.home();">
<i class="material-icons">arrow_back</i>
</material-button>
@@ -26,9 +26,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="item-count">{ registryUI.taglist.tags.length } tags</div>
</h2>
</div>
<div hide="{ registryUI.taglist.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
</material-card>
<div hide="{ registryUI.taglist.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
<pagination pages="{ registryUI.getPageLabels(this.page, registryUI.getNumPages(registryUI.taglist.tags)) }"></pagination>
<material-card ref="taglist-tag" class="taglist"
multi-delete={ this.multiDelete }
tags={ registryUI.getPage(registryUI.taglist.tags, this.page) }
show="{ registryUI.taglist.loadend }">
<table show="{ registryUI.taglist.loadend }" style="border: none;">
<thead>
<tr>
@@ -42,7 +48,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onclick="registryUI.taglist.reverse();">Tag
</th>
<th class="show-tag-history">History</th>
<th class={ 'remove-tag': true, delete: this.parent.toDelete > 0 } show="{ registryUI.isImageRemoveActivated }">
<th class={ 'remove-tag': true, delete: this.parent.toDelete > 0 } if="{ registryUI.isImageRemoveActivated }">
<material-checkbox ref="remove-tag-checkbox" class="indeterminate" show={ this.toDelete === 0} title="Toggle multi-delete. Alt+Click to select all tags."></material-checkbox>
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete selected images." onclick={ registryUI.taglist.bulkDelete } show={ this.toDelete > 0 }>
<i class="material-icons">delete</i>
@@ -50,7 +56,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</tr>
</thead>
<tbody>
<tr each="{ image in registryUI.taglist.tags }">
<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 }/>
@@ -67,15 +73,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<td class="show-tag-history">
<tag-history-button image={ image }/>
</td>
<td show="{ registryUI.isImageRemoveActivated }">
<td if="{ registryUI.isImageRemoveActivated }">
<remove-image multi-delete={ this.opts.multiDelete } image={ image }/>
</td>
</tr>
</tbody>
</table>
</material-card>
<pagination pages="{ registryUI.getPageLabels(this.page, registryUI.getNumPages(registryUI.taglist.tags)) }"></pagination>
<script>
var self = registryUI.taglist.instance = this;
self.page = registryUI.getPageQueryParam();
this.multiDelete = false;
this.toDelete = 0;
@@ -105,6 +113,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
});
this.on('page-update', function(page) {
self.page = page < 1 ? 1 : page;
registryUI.updateQueryString(registryUI.getQueryParams({ page: self.page }) );
this.toDelete = 0;
this.update();
});
this._getRemoveImageTags = function() {
var images = self.refs['taglist-tag'].tags['remove-image'];
if (!(images instanceof Array)) {
@@ -123,19 +138,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
};
this.on('mount', function() {
var toggle = this.tags['material-card'].refs['remove-tag-checkbox'].toggle;
this.tags['material-card'].refs['remove-tag-checkbox'].toggle = function(e) {
this.on('update', function() {
var checkbox = this.refs['taglist-tag'].refs['remove-tag-checkbox'];
if (!checkbox || checkbox._toggle) { return; }
checkbox._toggle = checkbox.toggle;
checkbox.toggle = function(e) {
if (e.altKey) {
self._getRemoveImageTags()
.filter(function(img) { return !img.tags['material-checkbox'].checked; })
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
} else {
toggle();
this._toggle();
}
};
this.tags['material-card'].refs['remove-tag-checkbox'].on('toggle', function() {
checkbox.on('toggle', function() {
registryUI.taglist.instance.multiDelete = this.checked;
registryUI.taglist.instance.update();
});
@@ -153,6 +171,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
registryUI.taglist.tags = registryUI.taglist.tags.map(function(tag) {
return new registryUI.DockerImage(registryUI.taglist.name, tag);
}).sort(registryUI.DockerImage.compare);
self.trigger('page-update', Math.min(self.page, registryUI.getNumPages(registryUI.taglist.tags)))
} else if (this.status == 404) {
registryUI.snackbar('Server not found', true);
} else {