feat: upgrade to riot v5 (#176)

See [migration guide](https://github.com/Joxit/docker-registry-ui/wiki/Migrating-from-1.x-to-2.x)
This commit is contained in:
Jones Magloire 2021-04-01 19:58:27 +02:00 committed by GitHub
commit 263584fc43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2149 additions and 1916 deletions

View file

@ -22,7 +22,7 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Standard Version
- name: Build and push Beta Version
uses: docker/build-push-action@v2
with:
context: .
@ -31,14 +31,4 @@ jobs:
push: true
tags: |
joxit/docker-registry-ui:master
joxit/docker-registry-ui:main
- name: Build and push Static Version
uses: docker/build-push-action@v2
with:
context: .
file: ./static.dockerfile
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
push: true
tags: |
joxit/docker-registry-ui:master-static
joxit/docker-registry-ui:main-static
joxit/docker-registry-ui:main

View file

@ -42,7 +42,7 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Standard Version
- name: Build and push Latest Version
uses: docker/build-push-action@v2
with:
context: .
@ -51,14 +51,4 @@ jobs:
push: true
tags: |
joxit/docker-registry-ui:latest
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}
- name: Build and push Static version
uses: docker/build-push-action@v2
with:
context: .
file: ./static.dockerfile
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
push: true
tags: |
joxit/docker-registry-ui:static
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}-static
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ registry-data
.idea
_site
*.orig
.serve/

View file

@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/

View file

@ -1 +0,0 @@
static.dockerfile

View file

@ -9,14 +9,11 @@ title: Docker Registry User Interface
## Overview
This project aims to provide a simple and complete user interface for your private docker registry.
You have the choice between two versions, the **standard interface** (`joxit/docker-registry-ui:latest`) and the **static interface** (`joxit/docker-registry-ui:static`).
:warning: `joxit/docker-registry-ui:master` and `joxit/docker-registry-ui:main` tags contains unreleased v2.0.0 version!
In the **standard interface**, there is no default registry, you need to add your own within the UI.
With this version, you can manage **more than one** registry server but all the environment variables will be **unavailable**.
All registries will be stored in the [local storage](https://en.wikipedia.org/wiki/Web_storage#Local_and_session_storage) of your browser. No configuration is needed when you launch the UI.
This project aims to provide a simple and complete user interface for your private docker registry. You can customize the interface with various options. The major option is `SINGLE_REGISTRY` which allows you to disable the dynamic selection of docker registeries (same behavior as the old **static** tag).
In the **static interface**, it will connect to a single registry and will not change. The configuration is done at the start of the interface, when you use the docker images whose tags contain the `static` keyword. With this version, you can manage **only one registry** and all environment variables will be **available**.
You may need the [migration guide from 1.x to 2.x](https://github.com/Joxit/docker-registry-ui/wiki/Migrating-from-1.x-to-2.x) or [the 1.x readme](https://github.com/Joxit/docker-registry-ui/blob/8fe3adf12540d1316cb57628ebe86a392a703d90/README.md)
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.
@ -40,20 +37,20 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- Display image/tag count (see [#56 issue comment](https://github.com/Joxit/docker-registry-ui/issues/56#issuecomment-449246524)).
- Select multiple tags to delete (see [#29](https://github.com/Joxit/docker-registry-ui/issues/29)).
- Select all tags with ALT + Click to delete (see [#80](https://github.com/Joxit/docker-registry-ui/issues/80)).
- One interface for many registries **standard interface**.
- Share your docker registry with query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`) **standard interface**.
- 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 via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) **static interface**
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)) **static interface**.
- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)) **static interface**.
- One interface for many registries (when `SINGLE_REGISTRY=false`).
- Share your docker registry with query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`) (when `SINGLE_REGISTRY=false`).
- Use the UI as reverse proxy (with `REGISTRY_URL` environment variable) to your docker registry (This will avoid CORS).
- Add Title when using `REGISTRY_URL` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)).
- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)).
- Add custom header via environment variable and file via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89))
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)).
- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)).
- Multi arch support in history page (see [#130](https://github.com/Joxit/docker-registry-ui/issues/130) and [#134](https://github.com/Joxit/docker-registry-ui/pull/134))
## FAQ
- What is the difference between **`joxit/docker-registry-ui:latest`** and **`joxit/docker-registry-ui:static`** tags ?
- The `latest` tag was the first version of the project, one UI for many docker registries. The `static` tag allows you to have an interface for a single registry and also allows you select your features.
- What is the difference between **`SINGLE_REGISTRY=false`** and **`SINGLE_REGISTRY=true`** options ?
- When `SINGLE_REGISTRY` is set to false, a menu appears on the interface allowing you to dynamically change docker registry URLs.
- Why, when I delete all tags of an image, the image is still in the UI ?
- This is a limitation of docker registry, the garbage collector don't remove empty images. If you want to delete dangling images, you will need to delete the folder in your registry data. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77))
- Why the image size in the UI is not the same as displayed during `docker images` ?
@ -80,46 +77,19 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples) or open an issue.
## Getting Started
The docker image contains the source code and nginx in order to serve the docker-registry-ui. Please remember the difference between the **standard interface** (`latest` tag) and **static interface** (`static` tags).
### Run the standard interface
You can run the standard interface see the website on your 80 port. You will be able to use the interface for **many registry servers**, but all the configuration via environment variables from the static interface will be **unavailable**.
```sh
docker run -d -p 80:80 joxit/docker-registry-ui:latest
```
### Run the static interface
## Available options
Some env options are available for use this interface for **only one server**.
- [`URL`](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone): set the static URL to use (You will need CORS configuration). Example: `http://127.0.0.1:5000`. (`Required`)
- [`REGISTRY_URL`](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy): your docker registry URL to contact (CORS configuration is not needed). Example: `http://my-docker-container:5000`. (Can't be used with `URL`, since 0.3.2).
- `DELETE_IMAGES`: if this variable is empty or `false`, delete feature is deactivated. It is activated otherwise.
- `REGISTRY_TITLE`: Set a custom title for your user interface when using `REGISTRY_URL` (since 0.3.4).
- `PULL_URL`: Set a custom url for the docker pull command, this is useful when you use `REGISTRY_URL` and your registry is on a different host (since 1.1.0).
- [`NGINX_PROXY_HEADER_*`](https://github.com/Joxit/docker-registry-ui/tree/main/examples/proxy-headers): Set custom headers for your docker registry, usefull when you want to add your credentials. (Can be use only with `REGISTRY_URL`).
- [`SHOW_CONTENT_DIGEST`](https://github.com/Joxit/docker-registry-ui/issues/126): Show content digest in docker tag list. Default: `true`.
- [`CATALOG_ELEMENTS_LIMIT`](https://github.com/Joxit/docker-registry-ui/pull/132): Limit the number of elements in the catalog page. Default: `100000`.
Example with `URL` option.
```sh
docker run -d -p 80:80 -e URL=http://127.0.0.1:5000 -e DELETE_IMAGES=true joxit/docker-registry-ui:static
```
Example with `REGISTRY_URL`, this will add a proxy to your registry.
Your registry will be accessible here : `http://127.0.0.1/v2`, this will avoid CORS errors (see [#25](https://github.com/Joxit/docker-registry-ui/issues/25#issuecomment-360522487)).
Be careful, `joxit/docker-registry-ui` and `registry:2` will communicate, both containers should be in the same network or use your private IP.
```sh
docker network create registry-ui-net
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
```
- `REGISTRY_URL`: The default url of your docker registry. You may need CORS configuration on your registry. (default: derived from the hostname of your UI).
- `REGISTRY_TITLE`: Set a custom title for your user interface. (default: value derived from `REGISTRY_URL`).
- `PULL_URL`: Set a custom url when you copy the `docker pull` command. (default: value derived from `REGISTRY_URL`).
- `DELETE_IMAGES`: Set if we can delete images from the UI. (default: `false`)
- `SHOW_CONTENT_DIGEST`: Show content digest in docker tag list. (default: `true`)
- `CATALOG_ELEMENTS_LIMIT`: Limit the number of elements in the catalog page. (default: `100000`).
- `SINGLE_REGISTRY`: Remove the menu that show the dialogs to add, remove and change the endpoint of your docker registry. (default `false`)
- `NGINX_PROXY_PASS_URL`: Update the default Nginx configuration and set the **proxy_pass** to your backend docker registry (this avoid CORS configuration).
- `NGINX_PROXY_HEADER_*`: Update the default Nginx configuration and set **custom headers** for your backend docker registry. Only when `NGINX_PROXY_PASS_URL` is used.
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/main/examples/ui-as-proxy/) or docker-registry-ui as standalone [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone/).
@ -185,7 +155,7 @@ http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['http://127.0.0.1:8001']
Access-Control-Allow-Origin: ['http://127.0.0.1:8000']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization', 'Accept']
Access-Control-Max-Age: [1728000]

View file

@ -1,27 +0,0 @@
# 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 arm32v7/nginx
LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
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 /docker-entrypoint.d/90-docker-registry-ui.sh
COPY favicon.ico /usr/share/nginx/html/

View file

@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/

View file

@ -1,27 +0,0 @@
# 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/
ENV NGINX_PROXY_HEADER_Host '$http_host'
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 /docker-entrypoint.d/90-docker-registry-ui.sh
COPY favicon.ico /usr/share/nginx/html/

View file

@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/

View file

@ -1,19 +1,20 @@
#!/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
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," index.html
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," index.html
sed -i "s,\${PULL_URL},${PULL_URL}," index.html
sed -i "s,\${SINGLE_REGISTRY},${SINGLE_REGISTRY}," index.html
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
sed -i -r "s/\${DELETE_IMAGES}/false/" index.html
fi
if [ "${SHOW_CONTENT_DIGEST}" = false ] ; then
sed -i -r "s/(showContentDigest[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
sed -i -r "s/\${SHOW_CONTENT_DIGEST}/false/" index.html
fi
if [ -n "${CATALOG_ELEMENTS_LIMIT}" ] ; then
sed -i -r "s/(catalogElementsLimit[:=])[^,;]*/\1${CATALOG_ELEMENTS_LIMIT}/" scripts/docker-registry-ui.js
sed -i -r "s/\${CATALOG_ELEMENTS_LIMIT}/${CATALOG_ELEMENTS_LIMIT}/" index.html
fi
get_nginx_proxy_headers() {
@ -33,8 +34,8 @@ get_nginx_proxy_headers() {
done
}
if [ -n "${REGISTRY_URL}" ] ; then
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," /etc/nginx/conf.d/default.conf
if [ -n "${NGINX_PROXY_PASS_URL}" ] ; then
sed -i "s,\${NGINX_PROXY_PASS_URL},${NGINX_PROXY_PASS_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

View file

@ -1,27 +0,0 @@
# 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 nginx:latest
LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
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 /docker-entrypoint.d/90-docker-registry-ui.sh
COPY favicon.ico /usr/share/nginx/html/

View file

@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/

View file

@ -19,12 +19,13 @@
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../dist/vendor.css">
<link rel="stylesheet" href="../dist/style.css">
<link rel="stylesheet" href="../dist/docker-registry-ui.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:locale" content="en_US" />
<meta name="description" content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
<meta property="og:description" content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
<meta name="description"
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
<meta property="og:description"
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
<link rel="canonical" href="https://joxit.dev/docker-registry-ui/demo/" />
<meta property="og:url" content="https://joxit.dev/docker-registry-ui/demo/" />
<meta property="og:site_name" content="Live Demo | Docker Registry User Interface" />
@ -35,26 +36,28 @@
</head>
<body>
<app></app>
<script src="../dist/docker-registry-ui.js"></script>
<script>
if ((function() {
try {
const res = JSON.parse(localStorage.getItem('registryServer'));
if (!(res instanceof Array) || res.length == 0) {
return true;
}
} catch (e) { return true; }
})()) {
localStorage.setItem('registryServer', JSON.stringify(['https://raw.githubusercontent.com/Joxit/docker-registry-ui/master/demo']))
if (DockerRegistryUI.getRegistryServers().length === 0) {
localStorage.setItem('registryServer', JSON.stringify([
'https://raw.githubusercontent.com/Joxit/docker-registry-ui/master/demo'
]))
}
var url = DockerRegistryUI.getUrlQueryParam() || DockerRegistryUI.getRegistryServers(0);
var tag = document.createElement('docker-registry-ui');
tag.setAttribute('registry-url', url);
tag.setAttribute('show-content-digest', true);
tag.setAttribute('is-image-remove-activated', true);
document.getElementsByTagName('body').item(0).appendChild(tag);
DockerRegistryUI.updateUrlQueryParam(url);
DockerRegistryUI.load();
</script>
<script src="../dist/scripts/vendor.js"></script>
<script src="../dist/scripts/docker-registry-ui.js"></script>
<script>
(function(i, s, o, g, r, a, m) {
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function() {
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),

View file

@ -27,7 +27,7 @@ server {
#! return 404;
#! }
#! ${NGINX_PROXY_HEADERS}
#! proxy_pass ${REGISTRY_URL};
#! proxy_pass ${NGINX_PROXY_PASS_URL};
#! }
#error_page 404 /404.html;

View file

@ -1,8 +1,9 @@
{
"name": "docker-registry-ui",
"version": "1.5.4",
"version": "2.0.0-beta.1",
"scripts": {
"build": "./node_modules/gulp/bin/gulp.js build",
"start": "ROLLUP_SERVE=true rollup -c -w",
"build": "rollup -c",
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
},
"repository": {
@ -12,24 +13,29 @@
"author": "Jones Magloire (Joxit)",
"license": "AGPL-3.0",
"description": "A web UI for private docker registry",
"dependencies": {},
"devDependencies": {
"del": "^3.0.0",
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-concat": "^2.6.0",
"gulp-filter": "^5.1.0",
"gulp-htmlmin": "^5.0.1",
"gulp-if": "^2.0.0",
"gulp-inject-version": "^1.0.1",
"gulp-license": "^1.1.0",
"gulp-riot": "^1.1.5",
"gulp-uglify": "^3.0.2",
"gulp-useref": "^3.1.6",
"riot": "^3.13.2",
"riot-mui": "^0.1.1",
"riot-route": "^3.1.4",
"stream-series": "^0.1.1",
"uglify-es": "^3.3.10"
"@babel/core": "^7.12.9",
"@babel/preset-env": "^7.12.7",
"@riotjs/compiler": "^5.3.1",
"@riotjs/observable": "^4.0.4",
"@riotjs/route": "^7.0.0",
"@rollup/plugin-babel": "^5.2.2",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-html": "^0.2.3",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"core-js": "^3.9.1",
"js-beautify": "^1.13.0",
"riot": "^5.3.1",
"riot-mui": "joxit/riot-5-mui#e3e993f",
"rollup": "^2.34.2",
"rollup-plugin-app-utils": "^1.0.6",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-riot": "^5.0.0",
"rollup-plugin-scss": "^2.6.1",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-styles": "^3.14.1",
"rollup-plugin-terser": "^7.0.2"
}
}

53
rollup.config.js Normal file
View file

@ -0,0 +1,53 @@
import riot from 'rollup-plugin-riot';
import nodeResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import { emptyDirectories } from 'rollup-plugin-app-utils';
import { babel } from '@rollup/plugin-babel';
import scss from 'rollup-plugin-scss';
import serve from 'rollup-plugin-serve';
import html from '@rollup/plugin-html';
import htmlUseref from './rollup/html-useref';
import json from '@rollup/plugin-json';
import copy from 'rollup-plugin-copy';
import copyTransform from './rollup/copy-transform';
import license from './rollup/license';
const useServe = process.env.ROLLUP_SERVE === 'true';
const output = useServe ? '.serve' : 'dist';
const plugins = [
riot(),
json(),
nodeResolve(),
commonjs(),
scss({ output: `./${output}/docker-registry-ui.css`, outputStyle: 'compressed' }),
babel({ babelHelpers: 'bundled', presets: [['@babel/env', { useBuiltIns: 'usage', corejs: { version: '2' } }]] }),
copy({
targets: [
{ src: 'src/fonts', dest: `${output}` },
{ src: 'src/images/*', dest: `${output}/images`, transform: copyTransform },
],
}),
];
if (useServe) {
plugins.push(serve({ host: 'localhost', port: 8000, contentBase: [output, './'] }));
} else {
plugins.push(terser({ format: { preamble: license } }));
}
export default [
{
input: { 'docker-registry-ui': 'src/index.js' },
output: {
dir: output,
name: 'DockerRegistryUI',
format: 'iife',
},
plugins: [emptyDirectories(output)].concat(
plugins,
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) })
),
},
];

6
rollup/copy-transform.js Normal file
View file

@ -0,0 +1,6 @@
export default function (contents, name) {
if (name.endsWith('.svg')) {
return contents.toString('utf8').split(/\n */).join(' ').replace(/\s+/g, ' ').trim();
}
return contents;
}

26
rollup/html-useref.js Normal file
View file

@ -0,0 +1,26 @@
import fs from 'fs';
const useref = /<!--\s*build:([a-z]+) ([-a-zA-Z./]+)\s*-->(.*?)<!--\s*endbuild\s*-->/ms;
const generateBalise = (type, output, body, opts = {}) => {
switch (type) {
case 'css':
return `<link href="${output}" rel="stylesheet" type="text/css">`;
case 'js':
return `<script src="${output}"></script>`;
case 'keep':
return opts[output] ? body : '';
}
};
export default function (src, opts) {
let html = fs
.readFileSync(src)
.toString()
.replace(/>\n+\s*/g, '>');
while (useref.test(html)) {
const [raw, type, output, body] = useref.exec(html);
html = html.replace(raw, generateBalise(type, output, body, opts));
}
return html;
}

18
rollup/license.js Normal file
View file

@ -0,0 +1,18 @@
export default `/*
* Copyright (C) 2016-2021 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/>.
*
* @license AGPL
*/`

View file

@ -0,0 +1,72 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<catalog-element>
<!-- Begin of tag -->
<div class="content">
<material-card class="list highlight" expanded="{state.expanded}" onclick="{ onClick }">
<material-waves onmousedown="{this.triggerLaunch}" center="true" color="#ddd"
setLaunchListener="{ setLaunchListener }" />
<span>
<i class="material-icons">send</i>
{ state.image || state.repo }
<div if="{state.images}" class="item-count right">
{ state.images.length } images
<i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i>
</div>
</span>
</material-card>
<catalog-element if="{ state.images }"
class="animated {!state.expanded ? 'hide' : ''} {state.expanding ? 'expanding' : ''}"
each="{item in state.images}" item="{ item }" />
</div>
<script>
import router from '../../scripts/router';
export default {
onBeforeMount(props, state) {
if (props.item.images && props.item.images.length === 1) {
state.image = props.item.images[0];
} else if (typeof props.item === 'string') {
state.image = props.item;
} else if (props.item.images && props.item.repo) {
state.images = props.item.images;
state.repo = props.item.repo;
}
},
onClick() {
const state = this.state;
if (!state.repo) {
router.taglist(state.image);
} else {
this.update({
expanded: !this.state.expanded,
expanding: true
});
setTimeout(() => {
this.update({
expanding: false
});
}, 50)
}
},
setLaunchListener(cb) {
this.triggerLaunch = cb;
}
}
</script>
<!-- End of tag -->
</catalog-element>

View file

@ -0,0 +1,100 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<catalog>
<material-card ref="catalog-tag" class="catalog header">
<div class="material-card-title-action">
<h2>
Repositories of { state.registryName }
<div class="item-count">{ state.length } images</div>
</h2>
</div>
</material-card>
<div if="{ !state.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
<catalog-element each="{ item in state.repositories }" item="{ item }" />
<script>
import CatalogElement from './catalog-element.riot'
import {
Http
} from '../../scripts/http';
export default {
components: {
CatalogElement
},
state: {
registryName: '',
length: 0,
loadend: false,
repositories: []
},
onBeforeMount(props) {
this.state.registryName = props.registryName;
this.state.catalogElementsLimit = props.catalogElementsLimit;
},
onMounted(props) {
this.display(props, this.state)
},
display(props, state) {
let repositories = [];
const self = this;
const oReq = new Http();
oReq.addEventListener('load', function () {
if (this.status == 200) {
repositories = JSON.parse(this.responseText).repositories || [];
repositories.sort();
repositories = repositories.reduce(function (acc, e) {
const slash = e.indexOf('/');
if (slash > 0) {
const repoName = e.substring(0, slash) + '/';
if (acc.length == 0 || acc[acc.length - 1].repo != repoName) {
acc.push({
repo: repoName,
images: []
});
}
acc[acc.length - 1].images.push(e);
return acc;
}
acc.push(e);
return acc;
}, []);
} else if (this.status == 404) {
self.props.onNotify('Server not found', true);
} else {
self.props.onNotify(this.responseText);
}
});
oReq.addEventListener('error', function () {
self.props.onNotify(this.getErrorMessage(), true);
});
oReq.addEventListener('loadend', function () {
self.update({
repositories,
length: repositories.length,
loadend: true
});
});
oReq.open('GET', `${props.registryUrl}/v2/_catalog?n=${state.catalogElementsLimit}`);
oReq.send();
}
}
</script>
</catalog>

View file

@ -0,0 +1,64 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<add-registry-url>
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
<div slot="title">Add your Server ?</div>
<div slot="content">
<material-input onkeyup="{ onKeyUp }" placeholder="Server URL"></material-input>
<span>Write your URL without /v2</span>
</div>
<div slot="action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ add }">
Add
</material-button>
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
Cancel
</material-button>
</div>
</material-popup>
<script>
import {
getRegistryServers
} from '../../scripts/utils';
import router from '../../scripts/router';
export default {
onKeyUp(event) {
// if keyCode is Enter
if (event.keyCode === 13) {
this.add();
}
},
add() {
const input = this.$('input');
if (!input || !input.value || input.value.length === 0) {
return this.props.onNotify('The input field is empty. Please enter an url.', true);
}
if (!input.value.startsWith('http')) {
return this.props.onNotify('The input field should start with http:// or https://.', true);
}
const url = input.value.trim().replace(/\/*$/, '');
const registryServer = getRegistryServers().filter(e => e !== url);
localStorage.setItem('registryServer', JSON.stringify([url].concat(registryServer)));
router.home()
this.props.onServerChange(url);
this.props.onClose()
setTimeout(() => router.updateUrlQueryParam(url), 100);
}
}
</script>
</add-registry-url>

View file

@ -0,0 +1,78 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<change-registry-url>
<material-popup opened="{ props.opened }" onClick="{ props.onClick }">
<div slot="title">Change your Server ?</div>
<div slot="content">
<select>
<option each="{ url in getRegistryServers() }" value="{ url }">{ url }</option>
</select>
</div>
<div slot="action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ change }">
Change
</material-button>
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
Cancel
</material-button>
</div>
</material-popup>
<script>
import {
getRegistryServers
} from '../../scripts/utils';
import router from '../../scripts/router';
export default {
change(event) {
const select = this.$('select');
if (!select || !select.value || select.value.length === 0) {
return this.props.onNotify('The select field is empty. Please add an url.', true);
}
if (!select.value.startsWith('http')) {
return this.props.onNotify('The select field should start with http:// or https://.', true);
}
const url = select.value.trim().replace(/\/*$/, '');
const registryServer = getRegistryServers().filter(e => e !== url);
localStorage.setItem('registryServer', JSON.stringify([url].concat(registryServer)));
router.home()
this.props.onServerChange(url);
this.props.onClose()
setTimeout(() => router.updateUrlQueryParam(url), 100);
},
getRegistryServers
}
</script>
<style>
:host select {
position: relative;
outline: 0;
box-shadow: none;
padding: 0;
width: 100%;
background: 0 0;
border: none;
font-weight: 400;
line-height: 24px;
height: 24px;
border-bottom: 1px solid #2f6975;
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
margin: 1.5em 0;
}
</style>
</change-registry-url>

View file

@ -0,0 +1,126 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<dialogs-menu>
<material-button onClick="{ onClick }" waves-center="true" rounded="true" waves-opacity="0.6" waves-duration="600">
<i class="material-icons">more_vert</i>
</material-button>
<material-dropdown-list items="{ dropdownItems }" onSelect="{ onDropdownSelect }"
opened="{ state.isDropdownOpened }" />
<div class="overlay" onclick="{ onClick }" if="{ state.isDropdownOpened }"></div>
<add-registry-url opened="{ state['add-registry-url'] }" on-close="{ onClose('add-registry-url') }"
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></add-registry-url>
<change-registry-url opened="{ state['change-registry-url'] }" on-close="{ onClose('change-registry-url') }"
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></change-registry-url>
<remove-registry-url opened="{ state['remove-registry-url'] }" on-close="{ onClose('remove-registry-url') }"
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></remove-registry-url>
<script>
import AddRegistryUrl from './add-registry-url.riot';
import ChangeRegistryUrl from './change-registry-url.riot';
import RemoveRegistryUrl from './remove-registry-url.riot';
export default {
components: {
AddRegistryUrl,
ChangeRegistryUrl,
RemoveRegistryUrl
},
dropdownItems: [{
title: 'Add URL',
name: 'add-registry-url'
}, {
title: 'Change URL',
name: 'change-registry-url'
}, {
title: 'Remove URL',
name: 'remove-registry-url'
}],
onDropdownSelect(key, item) {
this.update({
[item.name]: true,
isDropdownOpened: false
});
},
onClose(name) {
return () => {
this.update({
[name]: false,
isDropdownOpened: false
})
}
},
onClick() {
this.update({
isDropdownOpened: !this.state.isDropdownOpened
})
}
}
</script>
<style>
:host {
position: absolute;
top: 0px;
right: 16px;
color: #000;
list-style-type: disc;
margin-block-start: 0.7em;
}
:host .overlay {
position: fixed;
height: 100%;
width: 100%;
top: 0;
right: 0;
z-index: 10;
}
:host material-button {
background: rgba(255, 255, 255, 0);
float: right;
z-index: 2;
}
:host material-button .content i.material-icons {
color: #fff;
font-size: 24px;
}
:host material-dropdown-list {
display: inline-block;
position: relative;
}
:host material-dropdown-list ul.dropdown-content {
min-width: 156px;
padding: 8px 0;
margin: 0;
}
:host material-dropdown-list ul.dropdown-content li span {
font-size: 1rem;
line-height: 1.2em;
}
:host material-popup * {
line-height: 1em;
}
:host material-popup material-button .content {
line-height: 36px;
}
</style>
</dialogs-menu>

View file

@ -0,0 +1,66 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<remove-registry-url>
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
<div slot="title">Remove your Registry Server ?</div>
<div slot="content">
<ul class="list">
<li each="{ url in getRegistryServers() }">
<span>
<material-button onClick="{ remove }" url="{ url }" rounded="true" waves-color="rgba(158,158,158,.4)"
waves-center="true">
<i class="material-icons">delete</i>
</material-button>
<span class="url">{ url }</span>
</span>
</li>
</ul>
</div>
<div slot="action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
Close
</material-button>
</div>
</material-popup>
<script>
import {
getRegistryServers
} from '../../scripts/utils';
export default {
remove(event) {
const url = event.currentTarget.attributes.url && event.currentTarget.attributes.url.value;
const registryServer = getRegistryServers().filter(e => e !== url);
localStorage.setItem('registryServer', JSON.stringify(registryServer));
setTimeout(() => this.update(), 100);
},
getRegistryServers
}
</script>
<style>
:host material-popup .popup {
max-height: calc(95% - 2em);
}
:host material-popup .popup material-button {
margin-right: 1em;
}
:host material-popup .popup material-button .content i.material-icons {
color: #777;
}
</style>
</remove-registry-url>

View file

@ -0,0 +1,129 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<docker-registry-ui>
<header>
<material-navbar>
<div class="logo">Docker Registry UI</div>
<dialogs-menu if="{props.singleRegistry !== 'true'}" on-notify="{ notifySnackbar }"
on-server-change="{ onServerChange }"></dialogs-menu>
</material-navbar>
</header>
<main>
<router base="#!">
<route path="{baseRoute}">
<catalog registry-url="{ state.registryUrl }" registry-name="{ state.name }"
catalog-elements-limit="{ state.catalogElementsLimit }" on-notify="{ notifySnackbar }" />
</route>
<route path="{baseRoute}taglist/(.*)">
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
image="{ router.getTagListImage() }" show-content-digest="{props.showContentDigest}"
is-image-remove-activated="{props.isImageRemoveActivated}" on-notify="{ notifySnackbar }"></tag-list>
</route>
<route path="{baseRoute}taghistory/(.*)">
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
is-image-remove-activated="{props.isImageRemoveActivated}" on-notify="{ notifySnackbar }"></tag-history>
</route>
</router>
<material-snackbar message="{ state.snackbarMessage }" is-error="{ state.snackbarIsError }"></material-snackbar>
</main>
<footer>
<material-footer>
<a slot="logo" href="https://joxit.github.io/docker-registry-ui/">Docker Registry UI { version }</a>
<ul slot="link-list">
<li>
<a href="https://github.com/Joxit/docker-registry-ui">Contribute on GitHub</a>
</li>
<li>
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">Privacy &amp; Terms</a>
</li>
</ul>
</material-footer>
</footer>
<script>
import {
version
} from '../../package.json';
import {
Router,
Route,
} from '@riotjs/route'
import Catalog from './catalog/catalog.riot';
import TagList from './tag-list/tag-list.riot';
import TagHistory from './tag-history/tag-history.riot';
import DialogsMenu from './dialogs/dialogs-menu.riot';
import {
stripHttps,
getRegistryServers
} from '../scripts/utils';
import router from '../scripts/router';
export default {
components: {
Catalog,
TagList,
TagHistory,
DialogsMenu,
Router,
Route
},
onUpdated(props, state) {
state.snackbarIsError = false;
state.snackbarMessage = undefined;
},
onBeforeMount(props) {
// props.singleRegistry === 'true' means old static version
this.state.registryUrl = props.registryUrl ||
(props.singleRegistry === 'true' ? undefined : (router.getUrlQueryParam() || getRegistryServers(0))) ||
(window.location.origin + window.location.pathname.replace(/\/+$/, ''));
this.state.name = props.name || stripHttps(props.registryUrl);
this.state.catalogElementsLimit = props.catalogElementsLimit || 100000;
this.state.pullUrl = this.pullUrl(this.state.registryUrl, props.pullUrl);
},
onServerChange(registryUrl) {
this.update({
registryUrl,
name: stripHttps(registryUrl),
pullUrl: this.pullUrl(registryUrl),
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.'
})
},
pullUrl(registryUrl, pullUrl) {
const url = pullUrl ||
(registryUrl && registryUrl.length > 0 && registryUrl) ||
window.location.host;
return stripHttps(url);
},
notifySnackbar(message, isError) {
if (typeof message === 'string') {
this.update({
snackbarMessage: message,
snackbarIsError: isError || false
});
} else if (message && message.message) {
this.update({
snackbarMessage: message.message,
snackbarIsError: message.isError
});
}
},
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
router,
version
}
</script>
</docker-registry-ui>

View file

@ -0,0 +1,46 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<tag-history-element class="{ state.key }">
<div class="headline"><i class="material-icons">{ state.icon }</i>
<p>{ state.name }</p>
</div>
<div class="value" if="{ state.value }"> { state.value }</div>
<div class="value" each="{ value in state.values }" if="{ state.values }"> { value }</div>
<script>
import {
getHistoryIcon
} from '../../scripts/utils';
export default {
onBeforeStar(props, state) {
state.key = props.entry.key;
state.icon = getHistoryIcon(props.entry.key);
state.name = props.entry.key.replace('_', ' ');
if (props.value instanceof Array) {
state.values = props.entry.value;
} else {
state.value = props.entry.value;
}
},
onBeforeMount(props, state) {
this.onBeforeStar(props, state);
},
onBeforeUpdate(props, state) {
this.onBeforeStar(props, state);
}
}
</script>
</tag-history-element>

View file

@ -0,0 +1,185 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<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" onClick="{ toTaglist }">
<i class="material-icons">arrow_back</i>
</material-button>
<h2>
History of { props.image }:{ props.tag } <i class="material-icons">history</i>
</h2>
</div>
</material-card>
<div if="{ !state.loadend }" class="spinner-wrapper">
<material-spinner />
</div>
<material-tabs if="{ state.archs && state.loadend }" useLine="{ true }" tabs="{ state.archs }"
onTabChanged="{ onTabChanged }" />
<material-card each="{ element in state.elements }" class="tag-history-element">
<tag-history-element each="{ entry in element }" if="{ entry.value && entry.value.length > 0}" entry="{ entry }" />
</material-card>
<script>
import {
DockerImage
} from '../../scripts/docker-image';
import {
bytesToSize
} from '../../scripts/utils';
import router from '../../scripts/router';
import TagHistoryElement from './tag-history-element.riot'
export default {
components: {
TagHistoryElement
},
onBeforeMount(props, state) {
state.elements = [];
state.image = new DockerImage(props.image, props.tag, true, props.registryUrl, props.onNotify);
state.image.fillInfo()
state.image.on('blobs', this.processBlobs);
state.image.on('list', this.multiArchList)
},
onTabChanged(arch, idx) {
const state = this.state;
state.elements = []
state.image.variants[idx] = state.image.variants[idx] ||
new DockerImage(this.props.image, arch.digest, false, this.props.registryUrl, this.props.onNotify);
if (state.image.variants[idx].blobs) {
return this.processBlobs(state.image.variants[idx].blobs);
}
state.image.variants[idx].fillInfo();
state.image.variants[idx].on('blobs', this.processBlobs);
},
processBlobs(blobs) {
const state = this.state;
function exec(elt) {
const guiElements = [];
for (var attribute in elt) {
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
const value = elt[attribute];
const guiElement = {
"key": attribute,
"value": modifySpecificAttributeTypes(attribute, value)
};
guiElements.push(guiElement);
}
}
return guiElements.sort(eltSort);
}
const elements = new Array(blobs.history.length + 1);
elements[0] = exec(getConfig(blobs));
blobs.history.forEach(function (elt, i) {
elements[blobs.history.length - i] = exec(elt)
});
this.update({
elements,
loadend: true
});
},
multiArchList(manifests) {
manifests = manifests.manifests || manifests;
const archs = manifests.map(function (manifest) {
return {
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ?
manifest.platform.variant : ''),
digest: manifest.digest
}
});
this.update({
archs
});
},
toTaglist() {
router.taglist(this.props.image);
}
}
const eltIdx = function (e) {
switch (e) {
case 'created':
return 1;
case 'created_by':
return 2;
case 'size':
return 3;
case 'os':
return 4;
case 'architecture':
return 5;
case 'id':
return 6;
case 'linux':
return 7;
case 'docker_version':
return 8;
default:
return 10;
}
};
const eltSort = function (e1, e2) {
return eltIdx(e1.key) - eltIdx(e2.key);
};
const modifySpecificAttributeTypes = function (attribute, value) {
switch (attribute) {
case 'created':
return new Date(value).toLocaleString();
case 'created_by':
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
return (cmd && cmd[1]) || 'RUN'
case 'size':
return bytesToSize(value);
case 'Entrypoint':
case 'Cmd':
return (value || []).join(' ');
case 'Labels':
return Object.keys(value || {}).map(function (elt) {
return value[elt] ? elt + '=' + value[elt] : '';
});
case 'Volumes':
case 'ExposedPorts':
return Object.keys(value);
}
return value || '';
};
const getConfig = function (blobs) {
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels',
'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts'
]
.reduce(function (acc, e) {
const value = blobs[e] || blobs.config[e];
if (value && e === 'architecture' && blobs.variant) {
acc[e] = value + blobs.variant;
} else if (value) {
acc[e] = value;
}
return acc;
}, {});
if (!res.author && (res.Labels && res.Labels.maintainer)) {
res.author = blobs.config.Labels.maintainer;
delete res.Labels.maintainer;
}
return res;
};
</script>
</tag-history>

View file

@ -0,0 +1,58 @@
<!--
Copyright (C) 2016-2021 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>
<div class="copy-to-clipboard">
<input style="display: none; width: 1px; height: 1px;" value="{ state.dockerCmd }">
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ copy }"
title="Copy pull command.">
<i class="material-icons">content_copy</i>
</material-button>
</div>
<script>
import {
ERROR_CAN_NOT_READ_CONTENT_DIGEST
} from '../../scripts/utils';
export default {
onBeforeMount(props, state) {
const prefix = 'docker pull ' + props.pullUrl + '/' + props.image.name;
if (props.target === 'tag') {
state.dockerCmd = prefix + ':' + props.image.tag;
} else {
props.image.one('content-digest', (digest) => {
this.update({
dockerCmd: prefix + '@' + digest
})
});
props.image.trigger('get-content-digest');
}
},
copy() {
if (!this.state.dockerCmd) {
this.props.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
return;
}
const copyText = this.$('input');
copyText.style.display = 'block';
copyText.select();
document.execCommand('copy');
copyText.style.display = 'none';
this.props.onNotify('`' + this.state.dockerCmd + '` has been copied to clipboard.')
}
}
</script>
</copy-to-clipboard>

View file

@ -0,0 +1,50 @@
<!--
Copyright (C) 2016-2021 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-content-digest>
<div title="{ state.title }">{ state.displayId }</div>
<script>
export default {
onMounted(props) {
this.chars = -1;
props.image.one('content-digest', (digest) => {
this.digest = digest;
props.image.on('content-digest-chars', this.onResize);
props.image.trigger('get-content-digest-chars');
});
props.image.trigger('get-content-digest');
},
onResize(chars) {
if (chars === this.chars) {
return;
}
let displayId = this.digest;
let title = '';
this.chars = chars;
if (chars >= 70) {
displayId = this.digest;
} else if (chars <= 0) {
displayId = '';
title = this.digest;
} else {
displayId = this.digest.slice(0, chars) + '...';
title = this.digest;
}
this.update({title, displayId});
}
}
</script>
</image-content-digest>

View file

@ -1,5 +1,5 @@
<!--
Copyright (C) 2016-2019 Jones Magloire @Joxit
Copyright (C) 2016-2021 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
@ -15,16 +15,25 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<image-date>
<div title="Creation date { this.localDate }">{ registryUI.dateFormat(this.date) } ago</div>
<script type="text/javascript">
const self = this;
opts.image.on('creation-date', function(date) {
self.date = date;
self.localDate = date.toLocaleString()
self.update();
});
opts.image.trigger('get-date');
<div title="Creation date { state.localDate }">{ dateFormat(state.date) } ago</div>
<script>
import {
dateFormat,
} from '../../scripts/utils';
export default {
state: {
localDate: 'unknown'
},
onMounted(props) {
props.image.on('creation-date', (date) => {
this.update({
date: date,
localDate: date.toLocaleString()
});
});
props.image.trigger('get-date');
},
dateFormat
}
</script>
</image-date>

View file

@ -1,5 +1,5 @@
<!--
Copyright (C) 2016-2019 Jones Magloire @Joxit
Copyright (C) 2016-2021 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
@ -15,13 +15,21 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<image-size>
<div title="Compressed size of your image.">{ registryUI.bytesToSize(this.size) }</div>
<script type="text/javascript">
const self = this;
opts.image.on('size', function(size) {
self.size = size;
self.update();
});
opts.image.trigger('get-size');
<div title="Compressed size of your image.">{ bytesToSize(state.size) }</div>
<script>
import {
bytesToSize,
} from '../../scripts/utils';
export default {
onMounted(props) {
props.image.on('size', (size) => {
this.update({
size
});
});
props.image.trigger('get-size');
},
bytesToSize
}
</script>
</image-size>

View file

@ -1,5 +1,5 @@
<!--
Copyright (C) 2016-2019 Jones Magloire @Joxit
Copyright (C) 2016-2021 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
@ -15,13 +15,17 @@
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">
const self = this;
opts.image.on('sha256', function(sha256) {
self.sha256 = sha256.substring(0, 19);
self.update();
});
opts.image.trigger('get-sha256');
<div title="{ state.sha256 }">{ props.image.tag }</div>
<script>
export default {
onMounted(props) {
props.image.on('sha256', (sha256) => {
this.update({
sha256: sha256.substring(0, 19)
});
});
props.image.trigger('get-sha256');
}
}
</script>
</image-tag>

View file

@ -1,5 +1,5 @@
<!--
Copyright (C) 2016-2019 Jones Magloire @Joxit
Copyright (C) 2016-2021 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
@ -14,10 +14,17 @@ 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/>.
-->
<tag-history-element class="{entry.key}">
<div class="headline"><i class="material-icons">{ registryUI.getHistoryIcon(entry.key) }</i>
<p>{ entry.key.replace('_', ' ') }</p>
<pagination>
<div class="conatianer">
<div class="pagination-centered">
<material-button waves-color="rgba(158,158,158,.4)" each="{p in props.pages}"
class="{ p.current ? 'current' : ''} { p['space-left'] ? 'space-left' : '' } { p['space-right'] ? 'space-right' : ''}"
onClick="{() => props.onPageUpdate(p.page)}">
<i if="{ p.icon }" class="material-icons">{ p.icon }</i>
<div if="{ !p.icon }">{ p.page }</div>
</material-button>
</div>
</div>
<div class="value" if={!(entry.value instanceof Array)}> { entry.value }</div>
<div class="value" each={ e in entry.value } if={entry.value instanceof Array}> { e }</div>
</tag-history-element>
<script>
</script>
</pagination>

View file

@ -0,0 +1,84 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<remove-image>
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image."
if="{ !props.multiDelete }" disabled="{ !state.digest }" onClick="{ deleteImage }">
<i class="material-icons">delete</i>
</material-button>
<material-checkbox if="{ props.multiDelete }" title="Select this tag to delete it." disabled="{ !state.digest }"
onChange="{ handleCheckboxChange }" checked="{ state.checked }">
</material-checkbox>
<script>
import {
Http
} from '../../scripts/http';
import router from '../../scripts/router'
export default {
onBeforeMount(props, state) {
state.checked = props.checked;
props.image.one('content-digest', (digest) => {
this.update({
digest
});
});
props.image.trigger('get-content-digest');
},
onBeforeUpdate(props, state) {
state.checked = props.checked;
},
deleteImage(ignoreError) {
deleteImage(this.props.image, this.props.registryUrl, ignoreError, this.props.onNotify)
},
handleCheckboxChange(checked) {
this.props.handleCheckboxChange(checked, this.props.image);
}
}
export function deleteImage(image, registryUrl, ignoreError, onNotify) {
if (!image.digest) {
onNotify(`Information for ${name}:${tag} are not yet loaded.`);
return;
}
const name = image.name;
const tag = image.tag;
const oReq = new Http();
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
router.taglist(name);
onNotify(`Deleting ${name}:${tag} image. Run \`registry garbage-collect config.yml\` on your registry`);
} else if (this.status == 404) {
ignoreError || onNotify({
message: 'Digest not found for this image in your registry.',
isError: true
});
} else {
onNotify(this.responseText);
}
});
oReq.open('DELETE', registryUrl + '/v2/' + name + '/manifests/' + image.digest);
oReq.setRequestHeader('Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
oReq.addEventListener('error', function () {
onNotify({
message: 'An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].',
isError: true
});
});
oReq.send();
}
</script>
</remove-image>

View file

@ -1,5 +1,5 @@
<!--
Copyright (C) 2016-2019 Jones Magloire @Joxit
Copyright (C) 2016-2021 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
@ -15,16 +15,16 @@ 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-button>
<material-button ref="button" title="This will show the history of given tag" waves-center="true" rounded="true" waves-color="#ddd">
<material-button title="This will show the history of given tag" waves-center="true" rounded="true" waves-color="#ddd"
onClick="{ routeToHistory }">
<i class="material-icons">history</i>
</material-button>
<script type="text/javascript">
this.on('mount', function() {
const self = this;
this.refs.button.root.onclick = function() {
registryUI.taghistory.go(self.opts.image.name, self.opts.image.tag);
};
});
this.update()
<script>
import router from '../../scripts/router';
export default {
routeToHistory() {
router.history(this.props.image.name, this.props.image.tag)
}
}
</script>
</tag-history-button>

View file

@ -0,0 +1,161 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<tag-list>
<material-card class="header">
<div class="material-card-title-action ">
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ router.home }">
<i class="material-icons">arrow_back</i>
</material-button>
<h2>
Tags of { props.image }
<div class="source-hint">
Sourced from { state.registryName + '/' + props.image }
</div>
<div class="item-count">{ state.tags.length } tags</div>
</h2>
</div>
</material-card>
<div if="{ !state.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }"></tag-table>
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
<script>
import {
Http
} from '../../scripts/http';
import {
DockerImage,
compare
} from '../../scripts/docker-image';
import {
getNumPages,
getPageLabels
} from '../../scripts/utils'
import Pagination from './pagination.riot'
import TagTable from './tag-table.riot'
import router from '../../scripts/router'
export default {
components: {
Pagination,
TagTable,
},
onBeforeMount(props) {
this.state = {
registryName: props.registryName,
tags: [],
loadend: false,
asc: true,
page: router.getPageQueryParam() || 1
}
},
onMounted(props, state) {
this.display(props, state)
window.addEventListener('resize', this.onResize);
// this may be run before the final document size is available, so schedule
// a correction once everything is set up.
window.requestAnimationFrame(this.onResize);
},
display(props, state) {
state.tags = [];
const self = this;
const oReq = new Http();
oReq.addEventListener('load', function () {
state.tags = [];
if (this.status == 200) {
const tags = JSON.parse(this.responseText).tags || [];
state.tags = tags.map(function (tag) {
return new DockerImage(props.image, tag, null, props.registryUrl, props.onNotify);
}).sort(compare);
window.requestAnimationFrame(self.onResize);
self.update({
page: Math.min(state.page, getNumPages(state.tags))
})
} else if (this.status == 404) {
self.props.onNotify('Server not found', true);
} else {
self.props.onNotify(this.responseText, true);
}
});
oReq.addEventListener('error', function () {
self.props.onNotify(this.getErrorMessage(), true);
state.tags = [];
});
oReq.addEventListener('loadend', function () {
self.update({
loadend: true
});
});
oReq.open('GET', props.registryUrl + '/v2/' + props.image + '/tags/list');
oReq.send();
state.asc = true;
},
onPageUpdate(page) {
this.update({
page: page
});
router.updatePageQueryParam(page);
},
onResize() {
// window.innerWidth is a blocking access, cache its result.
const innerWidth = window.innerWidth;
let chars = 0;
const max = this.state.tags.reduce(function (acc, e) {
return e.tag.length > acc ? e.tag.length : acc;
}, 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);
}
if (max > 20) chars -= (max - 20);
chars = Math.floor(chars)
this.state.tags.map(function (image) {
image.trigger('content-digest-chars', chars);
});
},
onReverseOrder() {
if (this.state.asc) {
this.state.tags.reverse();
this.state.asc = false;
} else {
this.state.tags.sort(compare);
this.state.asc = true;
}
this.update();
},
getPageLabels,
getNumPages,
router
}
</script>
</tag-list>

View file

@ -0,0 +1,141 @@
<!--
Copyright (C) 2016-2021 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/>.
-->
<tag-table>
<material-card class="taglist">
<table style="border: none;">
<thead>
<tr>
<th class="creation-date">Creation date</th>
<th class="image-size">Size</th>
<th id="image-content-digest-header" if="{ props.showContentDigest }">Content Digest</th>
<th id="image-tag-header"
class="{ props.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
onclick="{() => props.onReverseOrder() }">Tag
</th>
<th class="show-tag-history">History</th>
<th class="remove-tag { state.toDelete.size > 0 ? 'delete' : '' }" if="{ props.isImageRemoveActivated }">
<material-checkbox class="indeterminate" checked="{ state.multiDelete }" if="{ state.toDelete.size === 0}"
title="Toggle multi-delete. Alt+Click to select all tags." onChange="{ onRemoveImageHeaderChange }">
</material-checkbox>
<material-button waves-center="true" rounded="true" waves-color="#ddd"
title="This will delete selected images." onClick="{ bulkDelete }" if="{ state.toDelete.size > 0 }">
<i class="material-icons">delete</i>
</material-button>
</th>
</tr>
</thead>
<tbody>
<tr each="{ image in getPage(props.tags, props.page) }">
<td class="creation-date">
<image-date image="{ image }" />
</td>
<td class="image-size">
<image-size image="{ image }" />
</td>
<td if="{ props.showContentDigest }">
<image-content-digest image="{ image }" />
<copy-to-clipboard target="digest" image="{ image }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }" />
</td>
<td>
<image-tag image="{ image }" />
<copy-to-clipboard target="tag" image="{ image }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }" />
</td>
<td class="show-tag-history">
<tag-history-button image="{ image }" />
</td>
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
on-notify="{ props.onNotify }" />
</td>
</tr>
</tbody>
</table>
</material-card>
<script>
import {
Http
} from '../../scripts/http';
import {
getPage,
} from '../../scripts/utils';
import ImageDate from './image-date.riot';
import ImageSize from './image-size.riot';
import ImageTag from './image-tag.riot';
import ImageContentDigest from './image-content-digest.riot';
import CopyToClipboard from './copy-to-clipboard.riot';
import TagHistoryButton from './tag-history-button.riot';
import RemoveImage, {
deleteImage
} from './remove-image.riot';
export default {
components: {
ImageDate,
ImageSize,
ImageTag,
ImageContentDigest,
CopyToClipboard,
RemoveImage,
TagHistoryButton,
},
onBeforeMount(props) {
this.state = {
toDelete: new Set(),
multiDelete: false,
page: props.page,
}
},
onBeforeUpdate(props, state) {
if (state.page !== props.page) {
state.toDelete.clear();
}
state.page = props.page
},
bulkDelete() {
this.state.toDelete.forEach(image => deleteImage(image, this.props.registryUrl, true, this.props.onNotify))
},
onRemoveImageHeaderChange(checked, event) {
if (event.altKey === true) {
const tags = getPage(this.props.tags, this.props.page);
tags.forEach(tag => this.state.toDelete.add(tag));
this.update({
multiDelete: true,
toDelete: this.state.toDelete
})
} else {
this.update({
multiDelete: checked
})
}
},
onRemoveImageChange(checked, image) {
if (checked) {
this.state.toDelete.add(image)
} else {
this.state.toDelete.delete(image)
}
this.update({
toDelete: this.state.toDelete
})
},
getPage
}
</script>
</tag-table>

View file

@ -1,5 +1,5 @@
<!--
Copyright (C) 2016-2019 Jones Magloire @Joxit
Copyright (C) 2016-2021 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
@ -19,10 +19,8 @@
<head>
<meta charset="UTF-8">
<!-- build:css vendor.css -->
<!-- build:css docker-registry-ui.css -->
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/css">
<!-- endbuild -->
<!-- build:css style.css -->
<link href="style.css" rel="stylesheet" type="text/css">
<link href="material-icons.css" rel="stylesheet" type="text/css">
<link href="roboto.css" rel="stylesheet" type="text/css">
@ -36,13 +34,21 @@
</head>
<body>
<app></app>
<!-- build:js scripts/vendor.js -->
<!-- build:keep production -->
<docker-registry-ui registry-url="${URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}">
</docker-registry-ui>
<!-- endbuild -->
<!-- build:keep developement -->
<docker-registry-ui registry-url="" name="Developement Registry" pull-url="" show-content-digest="true"
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false">
</docker-registry-ui>
<!-- endbuild -->
<!-- build:js docker-registry-ui.js -->
<script src="../node_modules/riot/riot+compiler.min.js"></script>
<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/docker-registry-ui.js -->
<script src="tags/catalog.riot" type="riot/tag"></script>
<script src="tags/catalog-element.riot" type="riot/tag"></script>
<script src="tags/tag-history-button.riot" type="riot/tag"></script>
@ -67,4 +73,4 @@
<!-- endbuild -->
</body>
</html>
</html>

39
src/index.js Normal file
View file

@ -0,0 +1,39 @@
import { component, register } from 'riot';
import {
MaterialCard,
MaterialSpinner,
MaterialNavbar,
MaterialFooter,
MaterialButton,
MaterialWaves,
MaterialCheckbox,
MaterialTabs,
MaterialSnackbar,
MaterialDropdownList,
MaterialPopup,
MaterialInput,
} from 'riot-mui';
import DockerRegistryUI from './components/docker-registry-ui.riot';
import './style.scss';
register('material-card', MaterialCard);
register('material-footer', MaterialFooter);
register('material-navbar', MaterialNavbar);
register('material-spinner', MaterialSpinner);
register('material-button', MaterialButton);
register('material-waves', MaterialWaves);
register('material-checkbox', MaterialCheckbox);
register('material-snackbar', MaterialSnackbar);
register('material-tabs', MaterialTabs);
register('material-dropdown-list', MaterialDropdownList);
register('material-popup', MaterialPopup);
register('material-input', MaterialInput);
const createApp = component(DockerRegistryUI);
const tags = document.getElementsByTagName('docker-registry-ui');
for (let i = 0; i < tags.length; i++) {
createApp(tags.item(i));
}

168
src/scripts/docker-image.js Normal file
View file

@ -0,0 +1,168 @@
/*
* Copyright (C) 2016-2021 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/>.
*/
import { Http } from './http';
import { isDigit, eventTransfer, ERROR_CAN_NOT_READ_CONTENT_DIGEST } from './utils';
import observable from '@riotjs/observable';
const tagReduce = (acc, e) => {
if (acc.length > 0 && isDigit(acc[acc.length - 1].charAt(0)) == isDigit(e)) {
acc[acc.length - 1] += e;
} else {
acc.push(e);
}
return acc;
};
export function compare(e1, e2) {
const tag1 = e1.tag.match(/./g).reduce(tagReduce, []);
const tag2 = e2.tag.match(/./g).reduce(tagReduce, []);
for (var i = 0; i < tag1.length && i < tag2.length; i++) {
const compare = tag1[i].localeCompare(tag2[i]);
if (isDigit(tag1[i].charAt(0)) && 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;
}
export class DockerImage {
constructor(name, tag, list, registryUrl, onNotify) {
this.name = name;
this.tag = tag;
this.list = list;
this.registryUrl = registryUrl;
this.chars = 0;
this.onNotify = onNotify;
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();
});
this.on('get-date', function () {
if (this.creationDate !== undefined) {
return this.trigger('creation-date', this.creationDate);
}
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();
});
}
fillInfo() {
if (this._fillInfoWaiting) {
return;
}
this._fillInfoWaiting = true;
const oReq = new Http();
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
const response = JSON.parse(this.responseText);
if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') {
self.trigger('list', response);
const manifest = response.manifests[0];
const image = new DockerImage(self.name, manifest.digest, false, self.registryUrl, self.onNotify);
eventTransfer(image, self);
image.fillInfo();
self.variants = [image];
return;
}
self.size = response.layers.reduce(function (acc, e) {
return acc + e.size;
}, 0);
self.sha256 = response.config.digest;
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);
if (!digest) {
self.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
}
});
self.getBlobs(response.config.digest);
} else if (this.status == 404) {
self.onNotify(`Manifest for ${self.name}:${self.tag} not found`, true);
} else {
self.onNotify(this.responseText);
}
});
oReq.open('GET', this.registryUrl + '/v2/' + self.name + '/manifests/' + self.tag);
oReq.setRequestHeader(
'Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' +
(self.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : '')
);
oReq.send();
}
getBlobs(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.blobs = response;
self.blobs.history
.filter(function (e) {
return !e.empty_layer;
})
.forEach(function (e, i) {
e.size = self.layers[i].size;
e.id = self.layers[i].digest.replace('sha256:', '');
});
self.blobs.id = blob.replace('sha256:', '');
self.trigger('creation-date', self.creationDate);
self.trigger('blobs', self.blobs);
} else if (this.status == 404) {
self.onNotify(`Blobs for ${self.name}:${self.tag} not found`, true);
} else {
self.onNotify(this.responseText);
}
});
oReq.open('GET', this.registryUrl + '/v2/' + self.name + '/blobs/' + blob);
oReq.setRequestHeader(
'Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json'
);
oReq.send();
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016-2019 Jones Magloire @Joxit
* Copyright (C) 2016-2021 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
@ -14,47 +14,47 @@
* 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.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
export class Http {
constructor() {
this.oReq = new XMLHttpRequest();
this.oReq.hasHeader = hasHeader;
this.oReq.getErrorMessage = getErrorMessage;
this._events = {};
this._headers = {};
}
};
Http.prototype.addEventListener = function(e, f) {
this._events[e] = f;
const self = this;
switch (e) {
case 'loadend':
{
self.oReq.addEventListener('loadend', function() {
getContentDigest(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
}
}
addEventListener(e, f) {
this._events[e] = f;
const self = this;
switch (e) {
case 'loadend': {
self.oReq.addEventListener('loadend', function () {
if (this.status == 401) {
const req = new XMLHttpRequest();
req._url = self._url;
req.open(self._method, self._url);
for (key in self._events) {
req.addEventListener(key, self._events[key]);
@ -73,53 +73,69 @@ Http.prototype.addEventListener = function(e, f) {
});
break;
}
case 'load':
{
self.oReq.addEventListener('load', function() {
case 'load': {
self.oReq.addEventListener('load', function () {
if (this.status !== 401) {
f.bind(this)();
}
});
break;
}
default:
{
self.oReq.addEventListener(e, function() {
default: {
self.oReq.addEventListener(e, function () {
f.bind(this)();
});
break;
}
}
}
setRequestHeader(header, value) {
this.oReq.setRequestHeader(header, value);
this._headers[header] = value;
}
open(m, u) {
this._method = m;
this._url = u;
this.oReq._url = u;
this.oReq.open(m, u);
}
send() {
this.oReq.send();
}
}
const hasHeader = function (header) {
return this.getAllResponseHeaders()
.split('\n')
.some(function (h) {
return new RegExp('^' + header + ':', 'i').test(h);
});
};
Http.prototype.setRequestHeader = function(header, value) {
this.oReq.setRequestHeader(header, value);
this._headers[header] = value;
};
Http.prototype.open = function(m, u) {
this._method = m;
this._url = u;
this.oReq.open(m, u);
};
Http.prototype.send = function() {
this.oReq.send();
};
Http.hasHeader = function(header) {
return this.getAllResponseHeaders().split('\n').some(function(h) {
return new RegExp('^' + header + ':', 'i').test(h);
});
};
Http.getErrorMessage = function() {
if (registryUI.url() && registryUI.url().match('^http://') && window.location.protocol === 'https:') {
return '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.';
} else if (!registryUI.url()) {
const getErrorMessage = function () {
if (this._url.match('^http://') && window.location.protocol === 'https:') {
return (
'Mixed Content: The page at `' +
window.location.origin +
'` was loaded over HTTPS, but requested an insecure server endpoint `' +
new URL(this._url).origin +
'`. This request has been blocked; the content must be served over HTTPS.'
);
} else if (!this._url || !this._url.match('^http')) {
return 'Incorrect server endpoint.';
} else if (this.withCredentials && !this.hasHeader('Access-Control-Allow-Credentials')) {
return '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.';
return (
"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 `" +
new URL(this._url).origin +
'` is therefore not allowed access.'
);
}
return 'An error occured: Check your connection and your registry must have `Access-Control-Allow-Origin` header set to `' + window.location.origin + '`';
};
return (
'An error occured: Check your connection and your registry must have `Access-Control-Allow-Origin` header set to `' +
window.location.origin +
'`'
);
};

104
src/scripts/router.js Normal file
View file

@ -0,0 +1,104 @@
/*
* Copyright (C) 2016-2021 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/>.
*/
import { router, getCurrentRoute } from '@riotjs/route';
import { encodeURI, decodeURI } from './utils';
function getQueryParams() {
const queries = {};
window.location.search
.slice(1)
.split('&')
.forEach((qs) => {
const splitIndex = qs.indexOf('=');
queries[qs.slice(0, splitIndex)] = splitIndex < 0 ? '' : qs.slice(splitIndex + 1);
});
return queries;
}
function updateQueryParams(qs) {
const queryParams = getQueryParams();
for (let key in qs) {
if (qs[key] === null) {
delete queryParams[key];
} else {
queryParams[key] = qs[key];
}
}
return queryParams;
}
function toSearchString(queries) {
let search = [];
for (let key in queries) {
if (key && queries[key] !== undefined) {
search.push(`${key}=${queries[key]}`);
}
}
return search.length === 0 ? '' : `?${search.join('&')}`;
}
function baseUrl(qs) {
const location = window.location;
const queryParams = updateQueryParams(qs);
return location.origin + location.pathname + toSearchString(queryParams);
}
export default {
home() {
router.push(baseUrl({ page: null }));
},
taglist(image) {
router.push(`${baseUrl({ page: null })}#!/taglist/${image}`);
},
getTagListImage() {
return getCurrentRoute().replace(/^.*(#!)?\/?taglist\//, '');
},
history(image, tag) {
router.push(`${baseUrl({ page: null })}#!/taghistory/image/${image}/tag/${tag}`);
},
getTagHistoryImage() {
return getCurrentRoute().replace(/^.*(#!)?\/?taghistory\/image\/(.*)\/tag\/(.*)\/?$/, '$2');
},
getTagHistoryTag() {
return getCurrentRoute().replace(/^.*(#!)?\/?taghistory\/image\/(.*)\/tag\/(.*)\/?$/, '$3');
},
updateQueryString(qs) {
const search = toSearchString(updateQueryParams(qs));
history.pushState(null, '', search + window.location.hash);
},
updateUrlQueryParam(url) {
this.updateQueryString({ url: encodeURI(url) });
},
getUrlQueryParam() {
const queries = getQueryParams();
const url = queries['url'];
if (url) {
try {
return decodeURI(url);
} catch (e) {
console.error(`Can't decode query parameter URL: ${url}`, e);
}
}
},
updatePageQueryParam(page) {
this.updateQueryString({ page });
},
getPageQueryParam() {
const queries = getQueryParams();
return queries['page'];
},
};

View file

@ -1,124 +0,0 @@
/*
* 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/>.
*/
var registryUI = {}
registryUI.URL_QUERY_PARAM_REGEX = /[&?]url=/;
registryUI.URL_PARAM_REGEX = /^url=/;
registryUI.showContentDigest = true;
registryUI.catalogElementsLimit = 100000;
registryUI.url = function(byPassQueryParam) {
if (!registryUI._url) {
const url = registryUI.getUrlQueryParam();
if (url) {
try {
registryUI._url = registryUI.decodeURI(url);
return registryUI._url;
} catch (e) {
console.log(e);
}
}
registryUI._url = registryUI.getRegistryServer(0);
}
return registryUI._url;
}
registryUI.name = function() {
return registryUI.stripHttps(registryUI.url());
}
registryUI.getRegistryServer = function(i) {
try {
const res = JSON.parse(localStorage.getItem('registryServer'));
if (res instanceof Array) {
return (!isNaN(i)) ? res[i] : res.map(function(url) {
return url.trim().replace(/\/*$/, '');
});
}
} catch (e) {}
return (!isNaN(i)) ? '' : [];
}
registryUI.addServer = function(url) {
const registryServer = registryUI.getRegistryServer();
url = url.trim().replace(/\/*$/, '');
const index = registryServer.indexOf(url);
if (index != -1) {
return;
}
registryServer.push(url);
if (!registryUI._url) {
registryUI.updateHistory(url);
}
localStorage.setItem('registryServer', JSON.stringify(registryServer));
};
registryUI.changeServer = function(url) {
var registryServer = registryUI.getRegistryServer();
url = url.trim().replace(/\/*$/, '');
const index = registryServer.indexOf(url);
if (index == -1) {
return;
}
registryServer.splice(index, 1);
registryServer = [url].concat(registryServer);
registryUI.updateHistory(url);
localStorage.setItem('registryServer', JSON.stringify(registryServer));
};
registryUI.removeServer = function(url) {
const registryServer = registryUI.getRegistryServer();
url = url.trim().replace(/\/*$/, '');
const index = registryServer.indexOf(url);
if (index == -1) {
return;
}
registryServer.splice(index, 1);
localStorage.setItem('registryServer', JSON.stringify(registryServer));
if (url == registryUI.url()) {
registryUI.updateHistory(registryUI.getRegistryServer(0));
route('');
}
}
registryUI.updateHistory = function(url) {
registryUI.updateQueryString({ url: registryUI.encodeURI(url) })
registryUI._url = url;
}
registryUI.getUrlQueryParam = function () {
const search = window.location.search;
if (registryUI.URL_QUERY_PARAM_REGEX.test(search)) {
const param = search.split(/^\?|&/).find(function(param) {
return param && registryUI.URL_PARAM_REGEX.test(param);
});
return param ? param.replace(registryUI.URL_PARAM_REGEX, '') : param;
}
};
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);
};
registryUI.isImageRemoveActivated = true;
registryUI.catalog = {};
registryUI.taglist = {};
registryUI.taghistory = {};
window.addEventListener('DOMContentLoaded', function() {
riot.mount('*');
});

View file

@ -1,44 +0,0 @@
/*
* 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/>.
*/
var registryUI = {}
registryUI.url = function() {
var url = '${URL}';
if (!url) {
url = window.location.origin + window.location.pathname;
return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
}
return url;
};
registryUI.name = function() {
const name = '${REGISTRY_TITLE}';
if (name) {
// the user can strip the http prefix if they wish
return name;
}
return registryUI.stripHttps(registryUI.url());
};
registryUI.pullUrl = '${PULL_URL}';
registryUI.isImageRemoveActivated = true;
registryUI.showContentDigest = true;
registryUI.catalogElementsLimit = 100000;
registryUI.catalog = {};
registryUI.taglist = {};
registryUI.taghistory = {};
window.addEventListener('DOMContentLoaded', function() {
riot.mount('*');
});

View file

@ -1,5 +1,4 @@
registryUI.bytesToSize = function (bytes) {
export function bytesToSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == undefined || isNaN(bytes)) {
return '?';
@ -8,13 +7,26 @@ registryUI.bytesToSize = function (bytes) {
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.ceil(bytes / Math.pow(1024, i)) + ' ' + sizes[i];
};
}
registryUI.dateFormat = function(date) {
export function dateFormat(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 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++) {
@ -24,10 +36,9 @@ registryUI.dateFormat = function(date) {
return Math.floor(diff / maxSeconds[i]) + ' ' + labels[i * 2 + 1];
}
}
};
}
registryUI.getHistoryIcon = function(attribute) {
export function getHistoryIcon(attribute) {
switch (attribute) {
case 'architecture':
return 'memory';
@ -63,29 +74,39 @@ registryUI.getHistoryIcon = function(attribute) {
case 'ExposedPorts':
return 'router';
default:
''
'';
}
}
registryUI.getPage = function(elts, page, limit) {
if (!limit) { limit = 100; }
if (!elts) { return []; }
export function getPage(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; }
export function getNumPages(elts, limit) {
if (!limit) {
limit = 100;
}
if (!elts) {
return 0;
}
return Math.trunc(elts.length / limit) + 1;
}
registryUI.getPageLabels = function(page, nPages) {
export function getPageLabels(page, nPages) {
var pageLabels = [];
var maxItems = 10;
if (nPages === 1) { return pageLabels; }
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});
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++) {
@ -93,35 +114,62 @@ registryUI.getPageLabels = function(page, nPages) {
page: i,
current: i === page,
'space-left': page === 1 && nPages > maxItems,
'space-right': page === nPages && 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});
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);
}
registryUI.stripHttps = function (url) {
export function stripHttps(url) {
if (!url) {
return '';
}
return url.replace(/^https?:\/\//, '');
}
export function eventTransfer(from, to) {
from.on('*', function (event, param) {
to.trigger(event, param);
});
}
export function isDigit(char) {
return char >= '0' && char <= '9';
}
export const ERROR_CAN_NOT_READ_CONTENT_DIGEST = {
message:
'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',
isError: true,
};
registryUI.eventTransfer = function(from, to) {
from.on('*', function(event, param) {
to.trigger(event, param);
})
export function getRegistryServers(i) {
try {
const res = JSON.parse(localStorage.getItem('registryServer'));
if (res instanceof Array) {
return !isNaN(i) ? res[i] : res.map((url) => url.trim().replace(/\/*$/, ''));
}
} catch (e) {}
return !isNaN(i) ? '' : [];
}
export function encodeURI(url) {
if (!url) {
return;
}
return url.indexOf('&') < 0 ? window.encodeURIComponent(url) : btoa(url);
}
export function decodeURI(url) {
if (!url) {
return;
}
return url.startsWith('http') ? window.decodeURIComponent(url) : atob(url);
}

View file

@ -14,7 +14,22 @@
* 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/>.
*/
@import 'riot-mui/src/material-elements/material-navbar/material-navbar.scss';
@import 'riot-mui/src/material-elements/material-footer/material-footer.scss';
@import 'riot-mui/src/material-elements/material-card/material-card.scss';
@import 'riot-mui/src/material-elements/material-spinner/material-spinner.scss';
@import 'riot-mui/src/material-elements/material-button/material-button.scss';
@import 'riot-mui/src/material-elements/material-waves/material-waves.scss';
@import 'riot-mui/src/material-elements/material-checkbox/material-checkbox.scss';
@import 'riot-mui/src/material-elements/material-tabs/material-tabs.scss';
@import 'riot-mui/src/material-elements/material-snackbar/material-snackbar.scss';
@import 'riot-mui/src/material-elements/material-dropdown-list/material-dropdown-list.scss';
@import 'riot-mui/src/material-elements/material-popup/material-popup.scss';
@import 'riot-mui/src/material-elements/material-input/material-input.scss';
@import './roboto.scss';
@import './material-icons.scss';
html > body {
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif !important;
}
@ -106,6 +121,10 @@ material-navbar {
height: 64px;
}
material-navbar nav-wrapper {
display: flex;
}
.logo {
padding: 0 16px 0 72px;
text-decoration: none;
@ -310,59 +329,6 @@ material-snackbar .toast {
height: auto;
}
menu {
position: absolute;
top: 0px;
right: 16px;
color: #000;
}
menu .overlay {
position: fixed;
height: 100%;
width: 100%;
top: 0;
right: 0;
z-index: 1;
}
#menu-control-button {
background: rgba(255, 255, 255, 0);
float: right;
}
#menu-control-button i {
color: #fff;
font-size: 24px;
}
#menu-control-dropdown {
display: inline-block;
position: relative;
}
.dropdown {
min-width: 124px;
padding: 8px 0;
margin: 0;
}
dropdown-item, #menu-control-dropdown p {
padding: 0 16px;
margin: auto;
line-height: 48px;
height: 48px;
cursor: pointer;
}
#menu-control-dropdown p:hover {
background-color: #eee;
}
#menu-control-dropdown p:active, .material-button-active:active {
background-color: #e0e0e0;
}
material-popup material-button,
pagination material-button {
background-color: #fff;
@ -392,27 +358,6 @@ material-footer {
padding: 0.5em 1em;
}
.select-padding {
padding: 20px 0;
}
select {
position: relative;
outline: 0;
box-shadow: none;
padding: 0;
width: 100%;
background: 0 0;
border: none;
font-weight: 400;
line-height: 24px;
height: 24px;
border-bottom: 1px solid #2f6975;
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
}
.copy-to-clipboard {
padding-left: 5px;
}

View file

@ -1,290 +0,0 @@
<!--
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/>.
-->
<app>
<header>
<material-navbar>
<div class="logo">Docker Registry UI</div>
<menu></menu>
</material-navbar>
</header>
<main>
<catalog if="{route.routeName == 'home'}"></catalog>
<taglist if="{route.routeName == 'taglist'}"></taglist>
<tag-history if="{route.routeName == 'taghistory'}"></tag-history>
<change></change>
<add></add>
<remove></remove>
<material-snackbar></material-snackbar>
</main>
<footer>
<material-footer>
<a class="material-footer-logo" href="https://joxit.github.io/docker-registry-ui/">Docker Registry UI
%%GULP_INJECT_VERSION%%</a>
<ul class="material-footer-link-list">
<li>
<a href="https://github.com/Joxit/docker-registry-ui">Contribute on GitHub</a>
</li>
<li>
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">Privacy &amp; Terms</a>
</li>
</ul>
</material-footer>
</footer>
<script type="text/javascript">
registryUI.appTag = this;
route.base('#!');
route('', function() {
route.routeName = 'home';
if (registryUI.catalog.display) {
registryUI.catalog.loadend = false;
}
registryUI.appTag.update();
});
route('/taglist/*', function(image) {
route.routeName = 'taglist';
registryUI.taglist.name = image;
if (registryUI.taglist.display) {
registryUI.taglist.loadend = false;
}
registryUI.appTag.update();
});
route('/taghistory/image/*/tag/*', function(image, tag) {
route.routeName = 'taghistory';
registryUI.taghistory.image = image;
registryUI.taghistory.tag = tag;
if (registryUI.taghistory.display) {
registryUI.taghistory.loadend = false;
}
registryUI.appTag.update();
});
registryUI.home = function() {
if (route.routeName == 'home') {
registryUI.catalog.display;
} else {
route('');
}
};
registryUI.taghistory.go = function(image, tag) {
route('/taghistory/image/' + image + '/tag/' + tag);
};
registryUI.snackbar = function(message, isError) {
registryUI.appTag.tags['material-snackbar'].addToast({'message': message, 'isError': isError}, 15000);
};
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);
};
route.parser(null, function(path, filter) {
const f = filter
.replace(/\?/g, '\\?')
.replace(/\*/g, '([^?#]+?)')
.replace(/\.\./, '.*');
const re = new RegExp('^' + f + '$');
const args = path.match(re);
if (args) return args.slice(1)
});
registryUI.isDigit = function(char) {
return char >= '0' && char <= '9';
};
registryUI.DockerImage = function(name, tag, list) {
this.name = name;
this.tag = tag;
this.list = list;
this.chars = 0;
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();
});
this.on('get-date', function() {
if (this.creationDate !== undefined) {
return this.trigger('creation-date', this.creationDate);
}
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) {
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) {
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() {
if (this._fillInfoWaiting) {
return;
}
this._fillInfoWaiting = true;
const oReq = new Http();
const self = this;
oReq.addEventListener('loadend', function() {
if (this.status == 200 || this.status == 202) {
const response = JSON.parse(this.responseText);
if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') {
self.trigger('list', response);
const manifest = response.manifests[0];
const image = new registryUI.DockerImage(self.name, manifest.digest)
registryUI.eventTransfer(image, self)
image.fillInfo()
self.variants = [image];
return;
}
self.size = response.layers.reduce(function(acc, e) {
return acc + e.size;
}, 0);
self.sha256 = response.config.digest;
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);
if (!digest) {
registryUI.showErrorCanNotReadContentDigest();
}
});
self.getBlobs(response.config.digest)
} 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, application/vnd.oci.image.manifest.v1+json'
+ (self.list ? ', application/vnd.docker.distribution.manifest.list.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.blobs = response;
self.blobs.history.filter(function(e) {
return !e.empty_layer;
}).forEach(function(e, i) {
e.size = self.layers[i].size;
e.id = self.layers[i].digest.replace('sha256:', '');
});
self.blobs.id = blob.replace('sha256:', '');
self.trigger('creation-date', self.creationDate);
self.trigger('blobs', self.blobs);
} 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, application/vnd.oci.image.manifest.v1+json');
oReq.send();
};
registryUI.taglist.go = function(image) {
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

@ -1,61 +0,0 @@
<!--
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/>.
-->
<catalog-element>
<!-- Begin of tag -->
<div class="content">
<material-card class="list highlight" item="{item}" expanded="{expanded}">
<material-waves onmousedown="{launch}" center="true" color="#ddd" />
<span>
<i class="material-icons">send</i>
{ typeof opts.item === "string" ? opts.item : opts.item.repo }
<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 if="{typeof opts.item !== "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
</div>
<script type="text/javascript">
this.on('mount', function() {
const self = this;
const card = this.tags['material-card'];
if (!card) {
return;
}
// Launch waves
card.launch = function(e) {
card.tags['material-waves'].trigger('launch',e);
}
if (this.item.images && this.item.images.length === 1) {
this.item = this.item.images[0];
}
card.root.onclick = function(e) {
if (!self.item.repo) {
registryUI.taglist.go(self.item);
} else {
self.expanded = !self.expanded;
self.update({expanded: self.expanded, expanding: true});
setTimeout(function() {
self.update({expanded: self.expanded, expanding: false});
}, 50)
}
}
})
</script>
<!-- End of tag -->
</catalog-element>

View file

@ -1,78 +0,0 @@
<!--
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/>.
-->
<catalog>
<!-- Begin of tag -->
<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>
</material-card>
<div hide="{ registryUI.catalog.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
<catalog-element each="{ item in registryUI.catalog.repositories }" />
<script type="text/javascript">
registryUI.catalog.instance = this;
registryUI.catalog.display = function() {
registryUI.catalog.repositories = [];
const oReq = new Http();
oReq.addEventListener('load', function() {
registryUI.catalog.repositories = [];
if (this.status == 200) {
if (!registryUI.url()) {
registryUI._url = window.location.origin + window.location.pathname.replace(/\/+$/, '')
}
registryUI.catalog.repositories = JSON.parse(this.responseText).repositories || [];
registryUI.catalog.repositories.sort();
registryUI.catalog.length = registryUI.catalog.repositories.length;
registryUI.catalog.repositories = registryUI.catalog.repositories.reduce(function(acc, e) {
const slash = e.indexOf('/');
if (slash > 0) {
const repoName = e.substring(0, slash) + '/';
if (acc.length == 0 || acc[acc.length - 1].repo != repoName) {
acc.push({repo: repoName, images: []});
}
acc[acc.length - 1].images.push(e);
return acc;
}
acc.push(e);
return acc;
}, []);
} else if (this.status == 404) {
registryUI.snackbar('Server not found', true);
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.addEventListener('error', function() {
registryUI.snackbar(this.getErrorMessage(), true);
registryUI.catalog.repositories = [];
});
oReq.addEventListener('loadend', function() {
registryUI.catalog.loadend = true;
registryUI.catalog.instance.update();
});
oReq.open('GET', registryUI.url() + '/v2/_catalog?n=' + registryUI.catalogElementsLimit);
oReq.send();
};
registryUI.catalog.display();
</script>
<!-- End of tag -->
</catalog>

View file

@ -1,50 +0,0 @@
<!--
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/>.
-->
<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.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();
document.execCommand('copy');
copyText.style.display = 'none';
registryUI.snackbar('`' + this.dockerCmd + '` has been copied to clipboard.')
};
</script>
</copy-to-clipboard>

View file

@ -1,59 +0,0 @@
<!--
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/>.
-->
<add>
<material-popup>
<div class="material-popup-title">Add your Server ?</div>
<div class="material-popup-content">
<material-input onkeyup="{ registryUI.addTag.onkeyup }" placeholder="Server URL"></material-input>
<span>Write your URL without /v2</span>
</div>
<div class="material-popup-action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="registryUI.addTag.add();">Add</material-button>
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="registryUI.addTag.close();">Cancel</material-button>
</div>
</material-popup>
<script type="text/javascript">
registryUI.addTag = registryUI.addTag || {};
this.one('mount', function () {
registryUI.addTag.dialog = this.tags['material-popup'];
registryUI.addTag.dialog.getAddServer = function() {
return this.tags['material-input'] ? this.tags['material-input'].value : '';
}
});
registryUI.addTag.onkeyup = function (e) {
// if keyCode is Enter
if (e.keyCode == 13) {
registryUI.addTag.add();
}
};
registryUI.addTag.show = function () {
registryUI.addTag.dialog.open();
};
registryUI.addTag.add = function () {
if (registryUI.addTag.dialog.getAddServer().length > 0) {
registryUI.addServer(registryUI.addTag.dialog.getAddServer());
}
registryUI.home();
registryUI.addTag.close();
};
registryUI.addTag.close = function () {
registryUI.addTag.dialog.tags['material-input'].value = '';
registryUI.addTag.dialog.close();
};
</script>
</add>

View file

@ -1,61 +0,0 @@
<!--
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/>.
-->
<change>
<material-popup>
<div class="material-popup-title">Change your Server ?</div>
<div class="material-popup-content">
<div class="select-padding">
<select class="mdl-textfield__input mdl-textfield__select" name="server-list" ref="server-list">
<option each="{ url in registryUI.getRegistryServer() }" value={url}>{url}</option>
</select>
</div>
</div>
<div class="material-popup-action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="registryUI.changeTag.change();">Change</material-button>
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="registryUI.changeTag.close();">Cancel</material-button>
</div>
</material-popup>
<script type="text/javascript">
registryUI.changeTag = registryUI.changeTag || {};
this.one('mount', function () {
registryUI.changeTag.dialog = this.tags['material-popup'];
registryUI.changeTag.dialog.getServerUrl = function () {
return this.refs['server-list']
? this.refs['server-list'].value
: '';
};
registryUI.changeTag.dialog.on('updated', function () {
if (this.refs['server-list']) {
this.refs['server-list'].value = registryUI.url();
}
});
});
registryUI.changeTag.show = function () {
registryUI.changeTag.dialog.open();
};
registryUI.changeTag.change = function () {
if (registryUI.changeTag.dialog.getServerUrl().length > 0) {
registryUI.changeServer(registryUI.changeTag.dialog.getServerUrl());
}
registryUI.home();
registryUI.changeTag.dialog.close();
};
registryUI.changeTag.close = function () {
registryUI.changeTag.dialog.close();
};
</script>
</change>

View file

@ -1,45 +0,0 @@
<!--
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/>.
-->
<menu>
<material-button id="menu-control-button" onclick="registryUI.menuTag.toggle();" waves-center="true" rounded="true" waves-opacity="0.6" waves-duration="600">
<i class="material-icons">more_vert</i>
</material-button>
<material-dropdown id="menu-control-dropdown">
<p onclick="registryUI.addTag.show(); registryUI.menuTag.close();">Add URL</p>
<p onclick="registryUI.changeTag.show(); registryUI.menuTag.close();">Change URL</p>
<p onclick="registryUI.removeTag.show(); registryUI.menuTag.close();">Remove URL</p>
</material-dropdown>
<div class="overlay" onclick="registryUI.menuTag.close();" show="{ registryUI.menuTag.isOpen && registryUI.menuTag.isOpen() }"></div>
<script type="text/javascript">
registryUI.menuTag = registryUI.menuTag || {};
registryUI.menuTag.update = this.update;
this.one('mount', function(args) {
const self = this;
registryUI.menuTag.close = function() {
self.tags['material-dropdown'].close();
self.update();
}
registryUI.menuTag.isOpen = function() {
return self.tags['material-dropdown'].opened;
}
registryUI.menuTag.toggle = function() {
self.tags['material-dropdown'].opened ? self.tags['material-dropdown'].close() : self.tags['material-dropdown'].open();
self.update();
};
});
</script>
</menu>

View file

@ -1,61 +0,0 @@
<!--
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/>.
-->
<remove>
<material-popup>
<div class="material-popup-title">Remove your Registry Server ?</div>
<div class="material-popup-content">
<ul class="list">
<li each="{ url in registryUI.getRegistryServer() }">
<span>
<a href="#" onClick="registryUI.removeTag.removeUrl('{url}');">
<i class="material-icons">delete</i>
</a>
<span class="url">{ url }</span>
</span>
</li>
</ul>
</div>
<div class="material-popup-action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="registryUI.removeTag.close();">
Close
</material-button>
</div>
</material-popup>
<script type="text/javascript">
registryUI.removeTag = registryUI.removeTag || {}
registryUI.removeTag.update = this.update;
registryUI.removeTag.removeUrl = function(url) {
registryUI.removeServer(url);
registryUI.removeTag.close();
};
registryUI.removeTag.close = function() {
registryUI.removeTag.dialog.close();
registryUI.removeTag.update();
};
registryUI.removeTag.show = function() {
registryUI.removeTag.dialog.open();
};
this.one('mount', function() {
registryUI.removeTag.dialog = this.tags['material-popup'];
});
</script>
</remove>

View file

@ -1,48 +0,0 @@
<!--
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>

View file

@ -1,40 +0,0 @@
<!--
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 type="text/javascript">
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

@ -1,78 +0,0 @@
<!--
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/>.
-->
<remove-image>
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." if="{ !opts.multiDelete }" disabled="{ !this.digest }">
<i class="material-icons">delete</i>
</material-button>
<material-checkbox if="{ opts.multiDelete }" title="Select this tag to delete it." disabled="{ !this.digest }"></material-checkbox>
<script type="text/javascript">
const self = this;
this.on('updated', function() {
if (self.multiDelete == self.opts.multiDelete) {
return;
}
if (self.tags['material-button']) {
self.delete = function(ignoreError) {
const name = self.opts.image.name;
const tag = self.opts.image.tag;
registryUI.taglist.go(name);
if (!self.digest) {
registryUI.snackbar('Information for ' + name + ':' + tag + ' are not yet loaded.');
return;
}
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 for this image in your registry.');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + self.digest);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+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();
};
self.tags['material-button'].root.onclick = function() {
self.delete();
}
}
if (self.tags['material-checkbox']) {
if (!self.opts.multiDelete && self.tags['material-checkbox'].checked) {
self.tags['material-checkbox'].toggle();
}
self.tags['material-checkbox'].on('toggle', function() {
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
});
}
self.multiDelete = self.opts.multiDelete;
});
opts.image.one('content-digest', function(digest) {
self.digest = digest;
self.update();
});
opts.image.trigger('get-content-digest');
</script>
</remove-image>

View file

@ -1,160 +0,0 @@
<!--
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/>.
-->
<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>
</material-button>
<h2>
History of { registryUI.taghistory.image }:{ registryUI.taghistory.tag } <i class="material-icons">history</i>
</h2>
</div>
</material-card>
<div hide="{ registryUI.taghistory.loadend }" class="spinner-wrapper">
<material-spinner />
</div>
<material-tabs if="{ this.archs }" useLine="true" tabs="{ this.archs }" tabchanged="{ this.tabchanged }" />
<material-card each="{ guiElement in this.elements }" class="tag-history-element">
<tag-history-element each="{ entry in guiElement }" if="{ entry.value && entry.value.length > 0}" />
</material-card>
<script type="text/javascript">
const self = this;
const eltIdx = function (e) {
switch (e) {
case 'created': return 1;
case 'created_by': return 2;
case 'size': return 3;
case 'os': return 4;
case 'architecture': return 5;
case 'id': return 6;
case 'linux': return 7;
case 'docker_version': return 8;
default: return 10;
}
};
const eltSort = function (e1, e2) {
return eltIdx(e1.key) - eltIdx(e2.key);
};
const modifySpecificAttributeTypes = function (attribute, value) {
switch (attribute) {
case 'created':
return new Date(value).toLocaleString();
case 'created_by':
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
return (cmd && cmd[1]) || 'RUN'
case 'size':
return registryUI.bytesToSize(value);
case 'Entrypoint':
case 'Cmd':
return (value || []).join(' ');
case 'Labels':
return Object.keys(value || {}).map(function (elt) {
return value[elt] ? elt + '=' + value[elt] : '';
});
case 'Volumes':
case 'ExposedPorts':
return Object.keys(value);
}
return value || '';
};
const getConfig = function (blobs) {
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels', 'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts']
.reduce(function (acc, e) {
const value = blobs[e] || blobs.config[e];
if (value && e === 'architecture' && blobs.variant) {
acc[e] = value + blobs.variant;
} else if (value) {
acc[e] = value;
}
return acc;
}, {});
if (!res.author && (res.Labels && res.Labels.maintainer)) {
res.author = blobs.config.Labels.maintainer;
delete res.Labels.maintainer;
}
return res;
};
const processBlobs = function (blobs) {
function exec(elt) {
const guiElements = [];
for (var attribute in elt) {
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
const value = elt[attribute];
const guiElement = {
"key": attribute,
"value": modifySpecificAttributeTypes(attribute, value)
};
guiElements.push(guiElement);
}
}
return guiElements.sort(eltSort);
}
self.elements = new Array(blobs.history.length + 1);
self.elements[0] = exec(getConfig(blobs));
blobs.history.forEach(function (elt, i) { self.elements[blobs.history.length - i] = exec(elt) });
registryUI.taghistory.loadend = true;
self.update();
};
const multiArchList = function (manifests) {
manifests = manifests.manifests || manifests;
self.archs = manifests.map(function (manifest) {
return {
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ? manifest.platform.variant : ''),
digest: manifest.digest
}
})
self.update();
};
self.tabchanged = function (arch, idx) {
self.elements = []
self.image.variants[idx] = self.image.variants[idx] || new registryUI.DockerImage(registryUI.taghistory.image, arch.digest);
if (self.image.variants[idx].blobs) {
return processBlobs(self.image.variants[idx].blobs);
}
self.image.variants[idx].fillInfo();
self.image.variants[idx].on('blobs', processBlobs);
};
registryUI.taghistory.display = function () {
self.elements = []
self.image = new registryUI.DockerImage(registryUI.taghistory.image, registryUI.taghistory.tag, true);
self.image.fillInfo()
self.image.on('blobs', processBlobs);
self.image.on('list', multiArchList)
};
this.on('mount', function () {
self.refs['tag-history-tag'].tags['material-button'].root.onclick = function () {
registryUI.taglist.go(registryUI.taghistory.image);
};
});
registryUI.taghistory.display();
self.update();
</script>
</tag-history>

View file

@ -1,245 +0,0 @@
<!--
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/>.
-->
<taglist>
<!-- Begin of tag -->
<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>
<h2>
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>
</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>
<th class="creation-date">Creation date</th>
<th class="image-size">Size</th>
<th id="image-content-digest-header" if="{ registryUI.showContentDigest }">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>
<th class="show-tag-history">History</th>
<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>
</material-button></th>
</tr>
</thead>
<tbody>
<tr each="{ image in this.opts.tags }">
<td class="creation-date">
<image-date image="{ image }"/>
</td>
<td class="image-size">
<image-size image="{ image }"/>
</td>
<td if="{ registryUI.showContentDigest }">
<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 }/>
</td>
<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 type="text/javascript">
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;
var max = registryUI.taglist.tags.reduce(function(acc, e) {
return e.tag.length > acc ? e.tag.length : acc;
}, 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);
}
if (max > 20) chars -= (max - 20);
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;
this.on('delete', function() {
if (!registryUI.isImageRemoveActivated || !this.multiDelete) {
return;
}
});
this.on('multi-delete', function() {
if (!registryUI.isImageRemoveActivated) {
return;
}
this.multiDelete = !this.multiDelete;
});
this.on('toggle-remove-image', function(checked) {
if (checked) {
this.toDelete++;
} else {
this.toDelete--;
}
if (this.toDelete <= 1) {
this.update();
}
});
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)) {
images = [images];
}
return images;
};
registryUI.taglist.bulkDelete = function(e) {
if (self.multiDelete && self.toDelete > 0) {
if (e.altKey) {
self._getRemoveImageTags()
.filter(function(img) { return img.tags['material-checkbox'].checked; })
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
}
self._getRemoveImageTags().filter(function(img) {
return img.tags['material-checkbox'].checked;
}).forEach(function(img) {
img.delete(true);
});
}
};
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) {
if (!this.checked) { this._toggle(); }
self._getRemoveImageTags()
.filter(function(img) { return !img.tags['material-checkbox'].checked; })
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
} else {
this._toggle();
}
};
checkbox.on('toggle', function() {
registryUI.taglist.instance.multiDelete = this.checked;
registryUI.taglist.instance.update();
});
});
registryUI.taglist.display = function() {
registryUI.taglist.tags = [];
if (route.routeName == 'taglist') {
const oReq = new Http();
registryUI.taglist.instance.update();
oReq.addEventListener('load', function() {
registryUI.taglist.tags = [];
if (this.status == 200) {
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);
} else {
registryUI.snackbar(this.responseText, true);
}
});
oReq.addEventListener('error', function() {
registryUI.snackbar(this.getErrorMessage(), true);
registryUI.taglist.tags = [];
});
oReq.addEventListener('loadend', function() {
registryUI.taglist.loadend = true;
registryUI.taglist.instance.update();
});
oReq.open('GET', registryUI.url() + '/v2/' + registryUI.taglist.name + '/tags/list');
oReq.send();
registryUI.taglist.asc = true;
}
};
registryUI.taglist.display();
registryUI.taglist.instance.update();
registryUI.taglist.reverse = function() {
if (registryUI.taglist.asc) {
registryUI.taglist.tags.reverse();
registryUI.taglist.asc = false;
} else {
registryUI.taglist.tags.sort(registryUI.DockerImage.compare);
registryUI.taglist.asc = true;
}
registryUI.taglist.instance.update();
};
</script>
<!-- End of tag -->
</taglist>

View file

@ -1,27 +0,0 @@
# 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 nginx:alpine
LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
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 /docker-entrypoint.d/90-docker-registry-ui.sh
COPY favicon.ico /usr/share/nginx/html/