Compare commits

...

10 Commits

Author SHA1 Message Date
Joxit
2f5e0dd307 Upgrade to v0.5.0 2018-11-20 21:56:14 +01:00
Jones Magloire
08c5eaee7e Merge pull request #51 from Joxit/feat/creation-date
[feat #49] Add creation date to taglist
2018-11-18 12:09:51 +01:00
Joxit
4cb79a670f [feat #49] Add creation date to taglist 2018-11-16 22:07:49 +01:00
Joxit
5173110883 Add new example for https://github.com/Joxit/docker-registry-ui/issues/26#issuecomment-415995589 2018-08-25 23:33:33 +02:00
Joxit
d3e93f7064 Fix demo for v0.4.0, sorry 😕 2018-08-04 18:58:36 +02:00
Joxit
6221958c78 Upgrade to v0.4.0
Merge tags and scripts; now it will be `docker-registry-ui.js` and `docker-registry-ui-static.js`
New sort for tags; will use numerical sort when it is possible
2018-07-24 00:05:45 +02:00
Jones Magloire
05c2cf2425 Merge pull request #46 from Joxit/new-tag-sort
New tag sort using digits
2018-07-23 20:36:04 +02:00
Joxit
82efd33c14 [new-tag-sort] Sort on numbers when there are instead of characters 2018-07-22 15:55:36 +02:00
Joxit
13936aadb1 [examples] Add ui-as-proxy example 2018-07-19 21:59:07 +02:00
Joxit
6bff056086 Add arm64v8 support 2018-07-18 22:59:07 +02:00
38 changed files with 517 additions and 180 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.project
node_modules
package-lock.json
registry-data

View File

@@ -24,9 +24,10 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- Use `joxit/docker-registry-ui:static` as reverse proxy to your docker registry (This will avoid CORS).
- Display image size (see #30)
- Add Title when using REGISTRY_URL (see #28)
- Alpine and Debian based images with supports for arm32v7
- Alpine and Debian based images with supports for arm32v7 and arm64v8
- Copy `docker pull` command to clipbloard
- Show sha256 for specific tag (hover image tag)
- Display image creation date (see #49)
## Getting Started
@@ -122,6 +123,8 @@ docker run -d --net registry-ui-net --name registry-srv registry:2
docker run -d --net registry-ui-net -p 80:80 -e REGISTRY_URL=http://registry-srv:5000 -e DELETE_IMAGES=true -e REGISTRY_TITLE="My registry" joxit/docker-registry-ui:static
```
There are some examples with [docker-compose](https://docs.docker.com/compose/) and docker-registry-ui as proxy [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy/) or docker-registry-ui as standalone [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone/).
## Using CORS
Your server should be configured to accept CORS.

View File

@@ -20,8 +20,7 @@ WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY dist/ /usr/share/nginx/html/
COPY dist/scripts/script-static.js /usr/share/nginx/html/scripts/script.js
COPY dist/scripts/tags-static.js /usr/share/nginx/html/scripts/tags.js
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
COPY bin/entrypoint /bin
ENTRYPOINT entrypoint

26
arm64v8-static.dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# 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/>.
FROM arm64v8/nginx
LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY dist/ /usr/share/nginx/html/
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
COPY bin/entrypoint /bin
ENTRYPOINT entrypoint

21
arm64v8.dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# 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/>.
FROM arm64v8/nginx
LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
COPY dist/ /usr/share/nginx/html/

View File

@@ -1,10 +1,10 @@
#!/bin/sh
$@
sed -i "s,\${URL},${URL}," scripts/script.js
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," scripts/script.js
sed -i "s,\${URL},${URL}," scripts/docker-registry-ui.js
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," scripts/docker-registry-ui.js
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i "s/registryUI.isImageRemoveActivated *= *[^,;]*/registryUI.isImageRemoveActivated=false/" scripts/script.js
sed -i "s/registryUI.isImageRemoveActivated *= *[^,;]*/registryUI.isImageRemoveActivated=false/" scripts/docker-registry-ui.js
fi
if [ -n "${REGISTRY_URL}" ] ; then

View File

@@ -20,8 +20,7 @@ WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY dist/ /usr/share/nginx/html/
COPY dist/scripts/script-static.js /usr/share/nginx/html/scripts/script.js
COPY dist/scripts/tags-static.js /usr/share/nginx/html/scripts/tags.js
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
COPY bin/entrypoint /bin
ENTRYPOINT entrypoint

View File

@@ -28,8 +28,7 @@
<body>
<app></app>
<script src="../dist/scripts/vendor.js"></script>
<script src="../dist/scripts/tags.js"></script>
<script src="../dist/scripts/script.js"></script>
<script src="../dist/scripts/docker-registry-ui.js"></script>
<script>
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;

2
dist/index.html vendored
View File

@@ -13,4 +13,4 @@
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/>.
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="vendor.css"><link rel="stylesheet" href="style.css"><link href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&amp;lang=en" rel="stylesheet" type="text/css"><title>Docker Registry UI</title></head><body><app></app><script src="scripts/vendor.js"></script><script src="scripts/tags.js"></script><script src="scripts/script.js"></script></body></html>
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="vendor.css"><link rel="stylesheet" href="style.css"><link href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&amp;lang=en" rel="stylesheet" type="text/css"><title>Docker Registry UI</title></head><body><app></app><script src="scripts/vendor.js"></script><script src="scripts/docker-registry-ui.js"></script></body></html>

File diff suppressed because one or more lines are too long

18
dist/scripts/docker-registry-ui.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +0,0 @@
/*!
* docker-registry-ui
* 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/>.
*/
function Http(){this.oReq=new XMLHttpRequest,this.oReq.hasHeader=Http.hasHeader,this.oReq.getErrorMessage=Http.getErrorMessage,this._events={},this._headers={}}Http.prototype.addEventListener=function(e,t){this._events[e]=t;var r=this;switch(e){case"loadend":r.oReq.addEventListener("loadend",function(){if(401==this.status){var e=new XMLHttpRequest;e.open(r._method,r._url);for(key in r._events)e.addEventListener(key,r._events[key]);for(key in r._headers)e.setRequestHeader(key,r._headers[key]);e.withCredentials=!0,e.hasHeader=Http.hasHeader,e.getErrorMessage=Http.getErrorMessage,e.send()}else t.bind(this)()});break;case"load":r.oReq.addEventListener("load",function(){401!==this.status&&t.bind(this)()});break;default:r.oReq.addEventListener(e,function(){t.bind(this)()})}},Http.prototype.setRequestHeader=function(e,t){this.oReq.setRequestHeader(e,t),this._headers[e]=t},Http.prototype.open=function(e,t){this._method=e,this._url=t,this.oReq.open(e,t)},Http.prototype.send=function(){this.oReq.send()},Http.hasHeader=function(e){return this.getAllResponseHeaders().split("\n").some(function(t){return new RegExp("^"+e+":","i").test(t)})},Http.getErrorMessage=function(){return registryUI.url()&&registryUI.url().match("^http://")&&"https:"===window.location.protocol?"Mixed Content: The page at `"+window.location.origin+"` was loaded over HTTPS, but requested an insecure server endpoint `"+registryUI.url()+"`. This request has been blocked; the content must be served over HTTPS.":registryUI.url()?this.withCredentials&&!this.hasHeader("Access-Control-Allow-Credentials")?"The `Access-Control-Allow-Credentials` header in the response is missing and must be set to `true` when the request's credentials mode is on. Origin `"+registryUI.url()+"` is therefore not allowed access.":"An error occured: Check your connection and your registry must have `Access-Control-Allow-Origin` header set to `"+window.location.origin+"`":"Incorrect server endpoint."};var registryUI={};registryUI.url=function(){return"${URL}"},registryUI.name=function(){return"${REGISTRY_TITLE}"},registryUI.isImageRemoveActivated=!0,registryUI.catalog={},registryUI.taglist={},riot.mount("catalog"),riot.mount("taglist"),riot.mount("app");

View File

@@ -1,18 +0,0 @@
/*!
* docker-registry-ui
* 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/>.
*/
function Http(){this.oReq=new XMLHttpRequest,this.oReq.hasHeader=Http.hasHeader,this.oReq.getErrorMessage=Http.getErrorMessage,this._events={},this._headers={}}Http.prototype.addEventListener=function(e,r){this._events[e]=r;var t=this;switch(e){case"loadend":t.oReq.addEventListener("loadend",function(){if(401==this.status){var e=new XMLHttpRequest;e.open(t._method,t._url);for(key in t._events)e.addEventListener(key,t._events[key]);for(key in t._headers)e.setRequestHeader(key,t._headers[key]);e.withCredentials=!0,e.hasHeader=Http.hasHeader,e.getErrorMessage=Http.getErrorMessage,e.send()}else r.bind(this)()});break;case"load":t.oReq.addEventListener("load",function(){401!==this.status&&r.bind(this)()});break;default:t.oReq.addEventListener(e,function(){r.bind(this)()})}},Http.prototype.setRequestHeader=function(e,r){this.oReq.setRequestHeader(e,r),this._headers[e]=r},Http.prototype.open=function(e,r){this._method=e,this._url=r,this.oReq.open(e,r)},Http.prototype.send=function(){this.oReq.send()},Http.hasHeader=function(e){return this.getAllResponseHeaders().split("\n").some(function(r){return new RegExp("^"+e+":","i").test(r)})},Http.getErrorMessage=function(){return registryUI.url()&&registryUI.url().match("^http://")&&"https:"===window.location.protocol?"Mixed Content: The page at `"+window.location.origin+"` was loaded over HTTPS, but requested an insecure server endpoint `"+registryUI.url()+"`. This request has been blocked; the content must be served over HTTPS.":registryUI.url()?this.withCredentials&&!this.hasHeader("Access-Control-Allow-Credentials")?"The `Access-Control-Allow-Credentials` header in the response is missing and must be set to `true` when the request's credentials mode is on. Origin `"+registryUI.url()+"` is therefore not allowed access.":"An error occured: Check your connection and your registry must have `Access-Control-Allow-Origin` header set to `"+window.location.origin+"`":"Incorrect server endpoint."};var registryUI={};registryUI.URL_QUERY_PARAM_REGEX=/[&?]url=/,registryUI.URL_PARAM_REGEX=/^url=/,registryUI.name=registryUI.url=function(e){if(!registryUI._url){var r=registryUI.getUrlQueryParam();if(r)try{return registryUI._url=registryUI.decodeURI(r),registryUI._url}catch(e){console.log(e)}registryUI._url=registryUI.getRegistryServer(0)}return registryUI._url},registryUI.getRegistryServer=function(e){try{var r=JSON.parse(localStorage.getItem("registryServer"));if(r instanceof Array)return isNaN(e)?r.map(function(e){return e.trim().replace(/\/*$/,"")}):r[e]}catch(e){}return isNaN(e)?[]:""},registryUI.addServer=function(e){var r=registryUI.getRegistryServer();e=e.trim().replace(/\/*$/,"");var t=r.indexOf(e);t==-1&&(r.push(e),registryUI._url||registryUI.updateHistory(e),localStorage.setItem("registryServer",JSON.stringify(r)))},registryUI.changeServer=function(e){var r=registryUI.getRegistryServer();e=e.trim().replace(/\/*$/,"");var t=r.indexOf(e);t!=-1&&(r.splice(t,1),r=[e].concat(r),registryUI.updateHistory(e),localStorage.setItem("registryServer",JSON.stringify(r)))},registryUI.removeServer=function(e){var r=registryUI.getRegistryServer();e=e.trim().replace(/\/*$/,"");var t=r.indexOf(e);t!=-1&&(r.splice(t,1),localStorage.setItem("registryServer",JSON.stringify(r)),e==registryUI.url()&&(registryUI.updateHistory(registryUI.getRegistryServer(0)),route("")))},registryUI.updateHistory=function(e){history.pushState(null,"",(e?"?url="+registryUI.encodeURI(e):"?")+window.location.hash),registryUI._url=e},registryUI.getUrlQueryParam=function(){var e=window.location.search;if(registryUI.URL_QUERY_PARAM_REGEX.test(e)){var r=e.split(/^\?|&/).find(function(e){return e&&registryUI.URL_PARAM_REGEX.test(e)});return r?r.replace(registryUI.URL_PARAM_REGEX,""):r}},registryUI.encodeURI=function(e){return e.indexOf("&")<0?window.encodeURIComponent(e):btoa(e)},registryUI.decodeURI=function(e){return e.startsWith("http")?window.decodeURIComponent(e):atob(e)},registryUI.isImageRemoveActivated=!0,registryUI.catalog={},registryUI.taglist={},riot.mount("*");

File diff suppressed because one or more lines are too long

18
dist/scripts/tags.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
# Docker Registry Static as proxy example
You can set up the static user interface as proxy in several ways.
If you want to populate your registry, use `populate.sh` script.
The interface and the docker registry will be accessible with <http://localhost>.
The simplest way is with `simple.yml` docker-compose file.
```sh
docker-compose -f simple.yml up -d
./populate.sh
```
You can add some credentials to access your registry wit `credentials.yml` docker-compose file.
Credentials for this example are login: `registry` and password: `ui` using bcrypt.
```sh
docker-compose -f credentials.yml up -d
./populate.sh
```

View File

@@ -0,0 +1,25 @@
version: '2.0'
services:
registry:
image: registry:2.6.2
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
depends_on:
- registry
networks:
- registry-ui-net
networks:
registry-ui-net:

View File

@@ -0,0 +1,17 @@
#!/bin/bash
docker tag joxit/docker-registry-ui:static localhost/joxit/docker-registry-ui:static
docker tag joxit/docker-registry-ui:static localhost/joxit/docker-registry-ui:0.3
docker tag joxit/docker-registry-ui:static localhost/joxit/docker-registry-ui:0.3.0
docker tag joxit/docker-registry-ui:static localhost/joxit/docker-registry-ui:0.3.0-static
docker tag joxit/docker-registry-ui:static localhost/joxit/docker-registry-ui:0.3-static
docker push localhost/joxit/docker-registry-ui
docker tag registry:2.6.2 localhost/registry:latest
docker tag registry:2.6.2 localhost/registry:2.6.2
docker tag registry:2.6.2 localhost/registry:2.6
docker tag registry:2.6.2 localhost/registry:2.6.0
docker tag registry:2.6.2 localhost/registry:2
docker push localhost/registry

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

@@ -0,0 +1,23 @@
version: '2.0'
services:
registry:
image: registry:2.6.2
volumes:
- ./registry-data:/var/lib/registry
networks:
- docker-registry-ui
ui:
image: joxit/docker-registry-ui:static
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
depends_on:
- registry
networks:
- docker-registry-ui
networks:
registry-ui-net:

View File

@@ -0,0 +1,22 @@
# Docker Registry Static as standalone example
You can set up the static user interface as standalone in several ways.
If you want to populate your registry, use `populate.sh` script.
The interface will be accessible with <http://localhost>.
Your docker registry will be accessible with <http://localhost:5000>.
The simplest way is with `simple.yml` docker-compose file.
```sh
docker-compose -f simple.yml up -d
./populate.sh
```
You can add some credentials to access your registry wit `credentials.yml` docker-compose file.
Credentials for this example are login: `registry` and password: `ui` using bcrypt.
```sh
docker-compose -f credentials.yml up -d
./populate.sh
```

View File

@@ -0,0 +1,20 @@
version: '2.0'
services:
registry:
image: registry:2.6.2
ports:
- 5000:5000
volumes:
- ./registry-data:/var/lib/registry
- ./registry-config/credentials.yml:/etc/docker/registry/config.yml
- ./registry-config/htpasswd:/etc/docker/registry/htpasswd
ui:
image: joxit/docker-registry-ui:static
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- URL=http://localhost:5000
depends_on:
- registry

View File

@@ -0,0 +1,17 @@
#!/bin/bash
docker tag joxit/docker-registry-ui:static localhost:5000/joxit/docker-registry-ui:static
docker tag joxit/docker-registry-ui:static localhost:5000/joxit/docker-registry-ui:0.3
docker tag joxit/docker-registry-ui:static localhost:5000/joxit/docker-registry-ui:0.3.0
docker tag joxit/docker-registry-ui:static localhost:5000/joxit/docker-registry-ui:0.3.0-static
docker tag joxit/docker-registry-ui:static localhost:5000/joxit/docker-registry-ui:0.3-static
docker push localhost:5000/joxit/docker-registry-ui
docker tag registry:2.6.2 localhost:5000/registry:latest
docker tag registry:2.6.2 localhost:5000/registry:2.6.2
docker tag registry:2.6.2 localhost:5000/registry:2.6
docker tag registry:2.6.2 localhost:5000/registry:2.6.0
docker tag registry:2.6.2 localhost:5000/registry:2
docker push localhost:5000/registry

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

@@ -0,0 +1,21 @@
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']

View File

@@ -0,0 +1,19 @@
version: '2.0'
services:
registry:
image: registry:2.6.2
ports:
- 5000:5000
volumes:
- ./registry-data:/var/lib/registry
- ./registry-config/simple.yml:/etc/docker/registry/config.yml
ui:
image: joxit/docker-registry-ui:static
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- URL=http://localhost:5000
depends_on:
- registry

View File

@@ -1,24 +1,48 @@
'use strict';
var cleanCSS = require('gulp-clean-css');
var concat = require('gulp-concat');
var del = require('del');
var filter = require('gulp-filter');
var fs = require('fs');
var gIf = require('gulp-if');
var gulp = require('gulp');
var htmlmin = require('gulp-htmlmin');
var license = require('gulp-license');
var riot = require('gulp-riot');
var minifier = require('gulp-uglify/minifier');
var uglify = require('uglify-js-harmony');
var useref = require('gulp-useref');
var injectVersion = require('gulp-inject-version');
const cleanCSS = require('gulp-clean-css');
const concat = require('gulp-concat');
const del = require('del');
const filter = require('gulp-filter');
const gIf = require('gulp-if');
const gulp = require('gulp');
const parallel = gulp.parallel;
const series = gulp.series;
const htmlmin = require('gulp-htmlmin');
const license = require('gulp-license');
const riot = require('gulp-riot');
const uglify = require('uglify-es');
const minifier = require('gulp-uglify/composer')(uglify);
const useref = require('gulp-useref');
const injectVersion = require('gulp-inject-version');
const merge = require('stream-series');
gulp.task('html', function() {
const allTags = 'src/tags/*.tag';
const allScripts = [
'src/scripts/http.js',
'src/scripts/script.js'
];
const staticTags = [
'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'
];
const staticScripts = [
'src/scripts/http.js',
'src/scripts/static.js'
];
function html() {
var htmlFilter = filter('**/*.html', {restore: true});
return gulp.src(['src/index.html'])
.pipe(useref())
.pipe(gIf(['*.js', '!*.min.js'], minifier({}, uglify))) // FIXME
.pipe(gIf(['*.js', '!*.min.js'], minifier())) // FIXME
.pipe(htmlFilter)
.pipe(htmlmin({
removeComments: false,
@@ -29,17 +53,16 @@ gulp.task('html', function() {
}))
.pipe(htmlFilter.restore)
.pipe(gulp.dest('dist'));
});
};
gulp.task('clean', function(done) {
function clean() {
return del(['dist']);
});
};
gulp.task('riot-tag', ['html'], function() {
return gulp.src('src/tags/*.tag')
.pipe(concat('tags.js'))
.pipe(riot())
.pipe(minifier({}, uglify))
function appStatic() {
return merge(gulp.src(staticScripts), gulp.src(staticTags).pipe(riot()))
.pipe(concat('docker-registry-ui-static.js'))
.pipe(minifier())
.pipe(license('agpl3', {
tiny: false,
project: 'docker-registry-ui',
@@ -48,13 +71,12 @@ gulp.task('riot-tag', ['html'], function() {
}))
.pipe(injectVersion())
.pipe(gulp.dest('dist/scripts'));
});
};
gulp.task('riot-static-tag', ['html'], function() {
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))
function app() {
return merge(gulp.src(allScripts), gulp.src(allTags).pipe(riot()))
.pipe(concat('docker-registry-ui.js'))
.pipe(minifier())
.pipe(license('agpl3', {
tiny: false,
project: 'docker-registry-ui',
@@ -63,41 +85,15 @@ gulp.task('riot-static-tag', ['html'], function() {
}))
.pipe(injectVersion())
.pipe(gulp.dest('dist/scripts'));
});
};
gulp.task('scripts-static', ['html'], function() {
return gulp.src(['src/scripts/http.js', 'src/scripts/static.js'])
.pipe(concat('script-static.js'))
.pipe(minifier({}, uglify))
.pipe(license('agpl3', {
tiny: false,
project: 'docker-registry-ui',
year: '2016-2018',
organization: 'Jones Magloire @Joxit'
}))
.pipe(gulp.dest('dist/scripts'));
});
gulp.task('scripts', ['html'], function() {
return gulp.src(['src/scripts/http.js', 'src/scripts/script.js'])
.pipe(concat('script.js'))
.pipe(minifier({}, uglify))
.pipe(license('agpl3', {
tiny: false,
project: 'docker-registry-ui',
year: '2016-2018',
organization: 'Jones Magloire @Joxit'
}))
.pipe(gulp.dest('dist/scripts'));
});
gulp.task('vendor', ['html'], function() {
function vendor() {
return gulp.src(['node_modules/riot/riot.min.js', 'node_modules/riot-route/dist/route.min.js', 'node_modules/riot-mui/build/js/riot-mui-min.js'])
.pipe(concat('vendor.js'))
.pipe(gulp.dest('dist/scripts'));
});
};
gulp.task('styles', ['html'], function() {
function styles() {
return gulp.src(['src/*.css'])
.pipe(concat('style.css'))
.pipe(cleanCSS({
@@ -110,18 +106,12 @@ gulp.task('styles', ['html'], function() {
organization: 'Jones Magloire @Joxit'
}))
.pipe(gulp.dest('dist/'));
});
};
gulp.task('fonts', function() {
function fonts() {
return gulp.src('src/fonts/*')
.pipe(filter('**/*.{otf,eot,svg,ttf,woff,woff2}'))
.pipe(gulp.dest('dist/fonts'));
});
};
gulp.task('sources', ['riot-tag', 'riot-static-tag', 'scripts', 'vendor', 'scripts-static', 'styles'], function() {
gulp.start();
});
gulp.task('build', ['clean'], function() {
gulp.start(['sources', 'fonts']);
});
exports.build = series(clean, html, parallel(fonts, styles, vendor, app, appStatic));

View File

@@ -1,6 +1,6 @@
{
"name": "docker-registry-ui",
"version": "0.3.10",
"version": "0.5.0",
"scripts": {
"build": "./node_modules/gulp/bin/gulp.js build"
},
@@ -14,21 +14,21 @@
"dependencies": {},
"devDependencies": {
"del": "^3.0.0",
"gulp": "^3.9",
"gulp-clean-css": "^3.9.4",
"gulp": "^4.0",
"gulp-clean-css": "^3.10.0",
"gulp-concat": "^2.6.0",
"gulp-filter": "^5.1.0",
"gulp-htmlmin": "^3.0.0",
"gulp-if": "^2.0.0",
"gulp-inject-version": "^1.0.1",
"gulp-license": "^1.1.0",
"gulp-riot": "^1.1.4",
"gulp-uglify": "^2.1.2",
"gulp-useref": "^3.1.5",
"riot": "^3.10.3",
"gulp-riot": "^1.1.5",
"gulp-uglify": "^3.0.1",
"gulp-useref": "^3.1.6",
"riot": "^3.13.1",
"riot-mui": "^0.1.1",
"riot-route": "^3.1.3",
"uglify-js": "^3.4.0",
"uglify-js-harmony": "^2.7.7"
"riot-route": "^3.1.4",
"stream-series": "^0.1.1",
"uglify-es": "^3.3.10"
}
}

View File

@@ -37,7 +37,7 @@
<script src="../node_modules/riot-route/dist/route.js"></script>
<script src="../node_modules/riot-mui/build/js/riot-mui.js"></script>
<!-- endbuild -->
<!-- build:js scripts/tags.js -->
<!-- build:js scripts/docker-registry-ui.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>
@@ -48,9 +48,8 @@
<script src="tags/remove.tag" type="riot/tag"></script>
<script src="tags/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/app.tag" type="riot/tag"></script>
<!-- endbuild -->
<!-- build:js scripts/script.js -->
<script src="scripts/http.js"></script>
<script src="scripts/script.js"></script>
<!-- endbuild -->

View File

@@ -111,4 +111,6 @@ registryUI.isImageRemoveActivated = true;
registryUI.catalog = {};
registryUI.taglist = {};
riot.mount('*');
window.addEventListener('DOMContentLoaded', function() {
riot.mount('*');
});

View File

@@ -25,6 +25,6 @@ registryUI.isImageRemoveActivated = true;
registryUI.catalog = {};
registryUI.taglist = {};
riot.mount('catalog');
riot.mount('taglist');
riot.mount('app');
window.addEventListener('DOMContentLoaded', function() {
riot.mount('*');
});

View File

@@ -78,7 +78,7 @@
return registryUI.snackbar(message, true);
}
registryUI.cleanName = function() {
var url = (registryUI.url() && registryUI.url().length > 0 && registryUI.url()) || window.location.host;
const url = (registryUI.url() && registryUI.url().length > 0 && registryUI.url()) || window.location.host;
if (url) {
return url.startsWith('http') ? url.replace(/https?:\/\//, '') : url;
}
@@ -94,6 +94,10 @@
if (args) return args.slice(1)
});
registryUI.isDigit = function(char) {
return char >= '0' && char <= '9';
}
registryUI.DockerImage = function (name, tag) {
this.name = name;
this.tag = tag;
@@ -110,10 +114,39 @@
}
return this.fillInfo();
});
this.on('get-date', function() {
if (this.date !== undefined) {
return this.trigger('date', this.date);
}
return this.fillInfo();
});
};
registryUI.DockerImage._tagReduce = function (acc, e) {
if (acc.length > 0 && registryUI.isDigit(acc[acc.length - 1].charAt(0)) == registryUI.isDigit(e)) {
acc[acc.length - 1] += e;
} else {
acc.push(e);
}
return acc;
}
registryUI.DockerImage.compare = function(e1, e2) {
return e1.tag.localeCompare(e2.tag);
const tag1 = e1.tag.match(/./g).reduce(registryUI.DockerImage._tagReduce, []);
const tag2 = e2.tag.match(/./g).reduce(registryUI.DockerImage._tagReduce, []);
for (var i = 0; i < tag1.length && i < tag2.length; i++) {
const compare = tag1[i].localeCompare(tag2[i]);
if (registryUI.isDigit(tag1[i].charAt(0)) && registryUI.isDigit(tag2[i].charAt(0))) {
const diff = tag1[i] - tag2[i];
if (diff != 0) {
return diff;
}
} else if (compare != 0) {
return compare;
}
}
return e1.tag.length - e2.tag.length;
};
registryUI.DockerImage.prototype.fillInfo = function() {
@@ -121,17 +154,18 @@
return;
}
this._fillInfoWaiting = true;
var oReq = new Http();
var self = this;
const oReq = new Http();
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
var response = JSON.parse(this.responseText);
const 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);
self.getBlobs(response.config.digest)
} else if (this.status == 404) {
registryUI.errorSnackbar('Manifest for ' + self.name + ':' + self.tag + ' not found');
} else {
@@ -142,6 +176,25 @@
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
}
registryUI.DockerImage.prototype.getBlobs = function(blob) {
const oReq = new Http();
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
const response = JSON.parse(this.responseText);
self.creationDate = new Date(response.created);
self.trigger('creation-date', self.creationDate);
} else if (this.status == 404) {
registryUI.errorSnackbar('Blobs for ' + self.name + ':' + self.tag + ' not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('GET', registryUI.url() + '/v2/' + self.name + '/blobs/' + blob);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
};
route.start(true);
</script>
</app>

43
src/tags/image-date.tag Normal file
View File

@@ -0,0 +1,43 @@
<!--
Copyright (C) 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-date>
<div title="Creation date { this.localDate }">{ this.dateFormat(this.date) } ago</div>
<script type="text/javascript">
var self = this;
this.dateFormat = function(date) {
if (date === undefined) {
return '';
}
const labels = ['a second', 'seconds', 'a minute', 'minutes', 'an hour', 'hours', 'a day', 'days', 'a month', 'months', 'a year', 'years'];
const maxSeconds = [1, 60, 3600, 86400, 2592000, 31104000, Infinity];
const diff = (new Date() - date) / 1000;
for (var i = 0; i < maxSeconds.length - 1; i++) {
if (maxSeconds[i] * 2 >= diff) {
return labels[i * 2];
} else if (maxSeconds[i + 1] > diff) {
return Math.floor(diff / maxSeconds[i]) + ' ' + labels[i * 2 + 1];
}
}
}
opts.image.on('creation-date', function(date) {
self.date = date;
self.localDate = date.toLocaleString()
self.update();
});
opts.image.trigger('get-date');
</script>
</image-date>

View File

@@ -31,6 +31,7 @@
<tr>
<th class="material-card-th-left">Repository</th>
<th></th>
<th>Creation date</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>
@@ -42,6 +43,7 @@
<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-tag image="{ image }" /></td>
<td show="{ registryUI.isImageRemoveActivated }">

View File

@@ -20,8 +20,7 @@ WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY dist/ /usr/share/nginx/html/
COPY dist/scripts/script-static.js /usr/share/nginx/html/scripts/script.js
COPY dist/scripts/tags-static.js /usr/share/nginx/html/scripts/tags.js
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
COPY bin/entrypoint /bin
ENTRYPOINT entrypoint