Compare commits

...

54 Commits
1.5.3 ... 2.0.2

Author SHA1 Message Date
Joxit
6c8b929e4f fix: delete button is displayed when DELETE_IMAGES is set to false
fixes #192
2021-05-20 18:49:39 +02:00
Joxit
4fcbea698b docs: update gif for 2.0 2021-05-12 18:05:40 +02:00
Joxit
999e5ae1c6 docs: more explanations for REGISTRY_URL and NGINX_PROXY_PASS_URL options 2021-05-11 10:19:15 +02:00
Joxit
8b9efa0397 fix: add default value for NGINX_PROXY_HEADER_Host
fixes #181
2021-05-08 21:32:31 +02:00
Joxit
1c88ad57f8 build: release 2.0.0 🚀 2021-04-28 23:54:03 +02:00
Joxit
c66887fac7 fix: wrong commit for riot-mui version 2021-04-27 10:02:01 +02:00
Jones Magloire
3dc035dac8 feat: show confirm dialog before deleting images (#182) 2021-04-27 09:52:34 +02:00
Joxit
29cd2b7a8f feat(confirm-delete): add dialog for single image delete 2021-04-26 22:31:11 +02:00
Joxit
e065298eed feat(confirm-delete): add dialog for confirmation of multiple image delete 2021-04-24 22:35:10 +02:00
Joxit
f02c99f12d chore: update riot-mui version for dialogs 2021-04-21 23:08:28 +02:00
Joxit
7eed05ae50 feat: show the number of images and repositories in catalog 2021-04-14 23:22:00 +02:00
callmebob2016
7a35d61359 docs: fix typo in ui-as-standalone example (#180) 2021-04-13 17:13:50 +02:00
Joxit
e990c39a18 feat: allow sorting of tags by creation date and size (#125)
closes #125
2021-04-13 05:43:23 +02:00
Jones Magloire
3bfe107e3c feat: add token authentication with keycloak (#167) 2021-04-12 00:43:15 +02:00
Joxit
0f54555b52 fix: env variables in entrypoint and blank demo page 2021-04-11 02:09:52 +02:00
Joxit
1ca1a1a207 docs(token-auth-keycloak): add tutorial for the example 2021-04-10 17:00:16 +02:00
Joxit
bc5082dcf9 feat(token-auth-keycloak): remove custom dialog and use browser basic auth with nginx configuration 2021-04-09 17:28:48 +02:00
Joxit
58b1486c81 feat(token-auth-keycloak): add token auth via keycloak using docker protocol 2021-04-08 18:11:08 +02:00
Joxit
2c9f006b8c docs(token-auth-keycloak): add missing ui in proxy configuration 2021-04-07 06:11:55 +02:00
Joxit
026cf8ccdc docs: add exemple for keycloak with token auth on docker registry 2021-04-07 06:11:55 +02:00
Joxit
69066dd867 ci: add debian latest and master/main version 2021-04-07 06:11:23 +02:00
Joxit
6d4d507db5 fix: lifecycle for copy to clipboard and remove image 2021-04-06 06:16:01 +02:00
Jones Magloire
83f15aa267 feat: add ability to search images and tags by name (#177) 2021-04-05 00:01:54 +02:00
Joxit
b643a44113 feat(search-bar): add search bar to tag-list, this will filter tag names 2021-04-04 05:59:15 +02:00
Joxit
a2b3c592df feat(search-bar): add search bar to catalog, this will filter images 2021-04-02 04:56:31 +02:00
Jones Magloire
263584fc43 feat: upgrade to riot v5 (#176)
See [migration guide](https://github.com/Joxit/docker-registry-ui/wiki/Migrating-from-1.x-to-2.x)
2021-04-01 19:58:27 +02:00
Joxit
1173453f72 feat: merge static version with latest and add license
BREAKING CHANGE: Now only one tag will be published: `latest`. This tag will include both the standard and latest version
2021-03-31 07:10:50 +02:00
Joxit
7d095916db fix(router): base route does not match ui on custom path 2021-03-30 21:47:28 +02:00
Joxit
8ef411059c feat(riot-v5): handle page query param & refactor router & remove old code 2021-03-29 21:49:37 +02:00
Joxit
603b5861fa feat(riot-v5): upgrade remove-registry-url dialog 2021-03-28 16:17:21 +02:00
Joxit
4ff1b2fabe feat(riot-v5): upgrade change-registry-url dialog 2021-03-27 14:02:10 +01:00
Joxit
e6af9321a8 feat(riot-v5): upgrade add-registry-url dialog 2021-03-26 00:04:28 +01:00
Joxit
bb3182d56e feat(riot-v5): add dialogs menu needs material-dropdown-list update 2021-03-25 20:08:02 +01:00
Joxit
761a680703 feat(riot-v5): add notification via material-snackbar 2021-03-24 00:02:07 +01:00
Joxit
926f67e1b5 feat(riot-v5): re-add ALT+click feature on tag list page 2021-03-23 23:08:59 +01:00
Joxit
781b026471 fix(riot-v5): on page change, selected images for deletion should be cleared
update riot-mui version
2021-03-22 07:59:35 +01:00
Joxit
92fd78165f fix(riot-v5): tag history with multi arch 2021-03-21 13:41:18 +01:00
Joxit
a3276dcb79 feat(riot-v5): upgrade all tag-history components 2021-03-20 08:15:05 +01:00
Joxit
962592c54a fix(riot-v5): fix catalog-element with multi items 2021-03-19 22:14:58 +01:00
Joxit
669c3399d0 feat(riot-v5): upgrade remove-image and copy-to-clipboard 2021-03-16 21:44:42 +01:00
Joxit
9c303d32c7 feat(riot-v5): upgrade image content-digest, date, size and tag from tag-list 2021-03-14 10:06:51 +01:00
Joxit
3d4267f5ab feat(riot-v5): add new tag-table tag
Now supports show-content-digest and is-image-remove-activated attributes
2021-03-10 23:32:25 +01:00
Joxit
b3427213d0 feat(riot-v5): start tag-list upgrade with pagination 2021-03-09 21:41:58 +01:00
Joxit
7beac85f25 feat(riot-v5): export docker image class and utils function 2021-03-07 11:24:50 +01:00
Joxit
ea21483346 feat(riot-v5): add config for router and register tag instead of import 2021-03-06 00:58:37 +01:00
Joxit
8fe3adf125 docs: fix read-only-auth example
fixes #175
2021-03-05 18:21:54 +01:00
Joxit
142727e8ac feat(riot-v5): upgrade catalog-element 2021-03-04 22:33:12 +01:00
Joxit
fb80283dd9 feat(riot-v5): upgrade catalog component + Http and fix global style 2021-03-03 00:05:44 +01:00
Joxit
11692c136e feat(riot-v5): create the base of the new UI 2021-03-02 01:15:20 +01:00
Joxit
cde932ec17 feat(riot-v5): create the skeleton for riot5 2021-03-01 23:55:32 +01:00
Joxit
307e171ede chore: fix issue 20 example typo and remove refs to master branch 2021-02-28 22:19:16 +01:00
Joxit
6b012cc8fd fix: add default favicon for the project: X/DNA from my signature.
closes #146
2021-02-14 00:01:52 +01:00
Joxit
887e77f2f1 chore(examples): update electron dependencies 2021-02-11 23:24:38 +01:00
Joxit
5890b723d2 ci(main): fix wrong step order... 2021-02-10 00:02:05 +01:00
123 changed files with 3179 additions and 2214 deletions

View File

@@ -4,4 +4,5 @@
!nginx
!src
!package.json
!gulpfile.js
!gulpfile.js
!favicon.ico

View File

@@ -8,11 +8,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Build the interface
run: npm run build
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
@@ -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 Alpine Version
uses: docker/build-push-action@v2
with:
context: .
@@ -32,13 +32,13 @@ jobs:
tags: |
joxit/docker-registry-ui:master
joxit/docker-registry-ui:main
- name: Build and push Static Version
- name: Build and push Beta Debian Version
uses: docker/build-push-action@v2
with:
context: .
file: ./static.dockerfile
file: ./debian.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:master-debian
joxit/docker-registry-ui:main-debian

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: .
@@ -52,13 +52,13 @@ jobs:
tags: |
joxit/docker-registry-ui:latest
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}
- name: Build and push Static version
- name: Build and push Latest Debian Version
uses: docker/build-push-action@v2
with:
context: .
file: ./static.dockerfile
file: ./debian.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:debian
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}-debian

1
.gitignore vendored
View File

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

View File

@@ -18,4 +18,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
COPY dist/ /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
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

114
README.md
View File

@@ -9,20 +9,15 @@ 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`).
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 **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.
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.
## [Project Page](https://joxit.dev/docker-registry-ui), [Live Demo](https://joxit.dev/docker-registry-ui/demo/), [Examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples)
## [Project Page](https://joxit.dev/docker-registry-ui), [Live Demo](https://joxit.dev/docker-registry-ui/demo/), [Examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples)
![preview](https://raw.github.com/Joxit/docker-registry-ui/master/docker-registry-ui.gif "Preview of Docker Registry UI")
![preview](https://raw.github.com/Joxit/docker-registry-ui/main/docker-registry-ui.gif "Preview of Docker Registry UI")
## Features
@@ -40,20 +35,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 `NGINX_PROXY_PASS_URL` environment variable) to your docker registry (This will avoid CORS).
- Add Title when using `REGISTRY_TITLE` (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` ?
@@ -71,57 +66,30 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- Why DELETE fails with 401 status code (using Basic Auth) ?
- This is caused by a bug in docker registry, I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104)).
- Can I use the docker registry ui as a standalone application (with Electron) ?
- Yes, check out the example [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron). (see [#129](https://github.com/Joxit/docker-registry-ui/pull/129))
- Yes, check out the example [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron). (see [#129](https://github.com/Joxit/docker-registry-ui/pull/129))
- I deleted images through the UI, but they are still present on the server. How can I delete them?
- When you delete an image with the UI, only the reference is deleted and not the content. To remove dangling images, you need to run the garbage collector of the registry with the command `registry garbage-collect config.yml` or `docker exec registry registry garbage-collect config.yml`. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77) [#147](https://github.com/Joxit/docker-registry-ui/issues/147))
- Why when I delete one tag, all tags with the same SHA are deleted ?
- This a docker registry API limitation, there is only one way to [delete images with tag](https://docs.docker.com/registry/spec/api/#deleting-an-image), it's by its `name` and its `manifest` (it's a sha of the content). So when you delete a tag, this will delete all tags of this image with the same SHA/manifest.
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples) or open an issue.
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/master/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/master/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/master/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`.
- `REGISTRY_URL`: The default url of your docker registry. You may need CORS configuration on your registry. This is usually the domain name or IP of your registry reachable by your computer (e.g `http://registry.example.com`). (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). This is usually the name of your registry container in the form `http://registry:5000`.
- `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.
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
```
There are some examples with [docker-compose](https://docs.docker.com/compose/) and docker-registry-ui as proxy [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy/) or docker-registry-ui as standalone [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone/).
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/).
## Using CORS
@@ -131,12 +99,12 @@ If your docker registry does not need credentials, you will need to send this HE
Access-Control-Allow-Origin: ['*']
If your docker registry need credentials, you will need to send these HEADERS:
If your docker registry need credentials, you will need to send these HEADERS (you must add the protocol `http`/`https` and the port when not default `80`/`443`):
```yml
http:
headers:
Access-Control-Allow-Origin: ['<your docker-registry-ui url>']
Access-Control-Allow-Origin: ['http://registry.example.com']
Access-Control-Allow-Credentials: [true]
Access-Control-Allow-Headers: ['Authorization', 'Accept']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS'] # Optional
@@ -185,7 +153,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]
@@ -203,14 +171,14 @@ check out the [Electron](examples/electron/README.md) standalone application.
## All examples
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone)
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/master/examples/traefik)
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/master/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy)
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone)
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/main/examples/traefik)
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/main/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/main/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))

View File

@@ -1,26 +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

View File

@@ -18,4 +18,9 @@ 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 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,26 +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

View File

@@ -18,4 +18,9 @@ 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 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,16 @@
#!/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
sed -i "s/\${CATALOG_ELEMENTS_LIMIT}/${CATALOG_ELEMENTS_LIMIT}/" index.html
sed -i "s/\${SHOW_CONTENT_DIGEST}/${SHOW_CONTENT_DIGEST}/" index.html
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
fi
if [ "${SHOW_CONTENT_DIGEST}" = false ] ; then
sed -i -r "s/(showContentDigest[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
fi
if [ -n "${CATALOG_ELEMENTS_LIMIT}" ] ; then
sed -i -r "s/(catalogElementsLimit[:=])[^,;]*/\1${CATALOG_ELEMENTS_LIMIT}/" scripts/docker-registry-ui.js
sed -i "s/\${DELETE_IMAGES}/false/" index.html
else
sed -i "s/\${DELETE_IMAGES}/true/" index.html
fi
get_nginx_proxy_headers() {
@@ -33,8 +30,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

@@ -6,8 +6,16 @@ for i in alpine chronograf:alpine consul debian jawg/mapnik3 nginx:alpine postgr
docker push 127.0.0.1:5000/$i
done
for i in arm32v7-static 1.2-debian-static master-static 1.2 arm64v8 arm32v7 arm64v8-static master 1.2-debian latest static debian-static debian 1.2-static 1.1 1.1-static 1.1-debian-static 1.1-debian ; do
for i in arm64v8 arm32v7 master latest debian main; do
docker pull joxit/docker-registry-ui:$i
docker tag joxit/docker-registry-ui:$i 127.0.0.1:5000/joxit/docker-registry-ui:$i
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$i
done
for v in 1.1 1.2 1.3 1.4 1.5 2.0 ; do
for type in "-debian" ""; do
docker pull joxit/docker-registry-ui:$v$type
docker tag joxit/docker-registry-ui:$v$type 127.0.0.1:5000/joxit/docker-registry-ui:$v$type
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$v$type
done
done

View File

@@ -1,26 +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

View File

@@ -18,4 +18,9 @@ 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 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,20 @@
</head>
<body>
<app></app>
<docker-registry-ui registry-url="" name="Demo Docker Registry UI" pull-url="" show-content-digest="true"
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false">
<script>
if (!localStorage.getItem('registryServer')) {
localStorage.setItem('registryServer', JSON.stringify([
'https://raw.githubusercontent.com/Joxit/docker-registry-ui/master/demo'
]))
}
</script>
<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']))
}
</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),

1
dist/docker-registry-ui.css vendored Normal file

File diff suppressed because one or more lines are too long

20
dist/docker-registry-ui.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" x="0px" y="0px" viewBox="-5.724 -43.601 730 600" fill="#777"><path d="m595.942,422.343 c -7.543,0.119 -13.562,6.331 -13.443,13.875 0.119,7.544 6.332,13.562 13.875,13.443 7.495,-0.118 13.494,-6.254 13.445,-13.75 -0.085,-7.578 -6.297,-13.652 -13.875,-13.568 0,0 -10e-4,0 0,0 m 0,24.398 c -5.975,0.272 -11.039,-4.352 -11.311,-10.326 -0.271,-5.976 4.352,-11.04 10.327,-11.312 5.975,-0.271 11.039,4.352 11.311,10.327 0.01,0.19 0.013,0.382 0.011,0.573 0.204,5.723 -4.27,10.527 -9.992,10.731 -0.115,0.005 -0.23,0.007 -0.346,0.007"/><path d="m599.081,436.342 v -0.185 c 1.512,-0.292 2.65,-1.544 2.8,-3.076 0.057,-1.175 -0.432,-2.311 -1.323,-3.077 -1.445,-0.765 -3.076,-1.106 -4.707,-0.984 -1.743,-0.024 -3.484,0.12 -5.2,0.431 v 13.538 h 3.077 v -5.446 h 1.477 c 1.754,0 2.554,0.646 2.83,2.154 0.184,1.143 0.536,2.252 1.047,3.292 h 3.415 c -0.53,-1.062 -0.873,-2.207 -1.016,-3.385 -0.138,-1.473 -1.088,-2.744 -2.462,-3.292 m -3.723,-0.985 h -1.508 v -3.908 c 0.583,-0.069 1.172,-0.069 1.754,0 1.97,0 2.893,0.831 2.893,2.062 0,1.231 -1.415,2 -3.076,2"/><path d="M707.494,193.557c-1.938-1.539-20.029-15.199-58.181-15.199c-10.074,0.044-20.127,0.908-30.061,2.584 c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908l-6.738,9.723c-8.438,13.061-14.598,27.459-18.214,42.582 c-6.831,28.891-2.677,56.027,11.999,79.226c-17.722,9.876-46.151,12.307-51.904,12.522H22.367 c-12.294,0.017-22.27,9.952-22.337,22.245c-0.549,41.234,6.437,82.222,20.614,120.946c16.214,42.521,40.336,73.842,71.719,93.01 c35.167,21.537,92.302,33.844,157.067,33.844c29.258,0.092,58.461-2.556,87.226-7.907c39.986-7.342,78.463-21.318,113.839-41.352 c29.149-16.88,55.383-38.354,77.688-63.596c37.29-42.213,59.505-89.226,76.026-131.007c2.215,0,4.431,0,6.584,0 c40.828,0,65.935-16.338,79.78-30.029c9.201-8.732,16.384-19.369,21.045-31.167l2.923-8.553L707.494,193.557z"/><path d="M65.995,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.476-5.538 c-0.01,0-0.021,0-0.031,0H65.995c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C60.488,226.443,62.953,228.909,65.995,228.909L65.995,228.909"/><path d="M152.913,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.059,0-5.538,2.479-5.538,5.538v56.181C147.392,226.448,149.866,228.909,152.913,228.909"/><path d="M241.153,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C235.646,226.443,238.112,228.909,241.153,228.909L241.153,228.909"/><path d="M328.348,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C322.841,226.443,325.307,228.909,328.348,228.909L328.348,228.909"/><path d="M152.913,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.046,0-5.521,2.46-5.538,5.507v56.181C147.392,145.597,149.861,148.066,152.913,148.083"/><path d="M241.153,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C235.646,145.591,238.107,148.066,241.153,148.083"/><path d="M328.348,148.083h63.073c3.052-0.017,5.521-2.486,5.538-5.538V86.364c-0.017-3.047-2.491-5.507-5.538-5.507h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C322.841,145.591,325.302,148.066,328.348,148.083"/><path d="M328.348,67.227h63.073c3.047,0,5.521-2.461,5.538-5.507V5.507C396.942,2.46,394.468,0,391.421,0h-63.073 c-3.042,0-5.507,2.465-5.507,5.507l0,0v56.212C322.841,64.761,325.307,67.227,328.348,67.227"/><path d="M416.312,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.041,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C410.805,226.443,413.271,228.909,416.312,228.909"/></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" x="0px" y="0px" viewBox="-5.724 -43.601 730 600" fill="#777"> <path d="m595.942,422.343 c -7.543,0.119 -13.562,6.331 -13.443,13.875 0.119,7.544 6.332,13.562 13.875,13.443 7.495,-0.118 13.494,-6.254 13.445,-13.75 -0.085,-7.578 -6.297,-13.652 -13.875,-13.568 0,0 -10e-4,0 0,0 m 0,24.398 c -5.975,0.272 -11.039,-4.352 -11.311,-10.326 -0.271,-5.976 4.352,-11.04 10.327,-11.312 5.975,-0.271 11.039,4.352 11.311,10.327 0.01,0.19 0.013,0.382 0.011,0.573 0.204,5.723 -4.27,10.527 -9.992,10.731 -0.115,0.005 -0.23,0.007 -0.346,0.007"/> <path d="m599.081,436.342 v -0.185 c 1.512,-0.292 2.65,-1.544 2.8,-3.076 0.057,-1.175 -0.432,-2.311 -1.323,-3.077 -1.445,-0.765 -3.076,-1.106 -4.707,-0.984 -1.743,-0.024 -3.484,0.12 -5.2,0.431 v 13.538 h 3.077 v -5.446 h 1.477 c 1.754,0 2.554,0.646 2.83,2.154 0.184,1.143 0.536,2.252 1.047,3.292 h 3.415 c -0.53,-1.062 -0.873,-2.207 -1.016,-3.385 -0.138,-1.473 -1.088,-2.744 -2.462,-3.292 m -3.723,-0.985 h -1.508 v -3.908 c 0.583,-0.069 1.172,-0.069 1.754,0 1.97,0 2.893,0.831 2.893,2.062 0,1.231 -1.415,2 -3.076,2"/> <path d="M707.494,193.557c-1.938-1.539-20.029-15.199-58.181-15.199c-10.074,0.044-20.127,0.908-30.061,2.584 c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908l-6.738,9.723c-8.438,13.061-14.598,27.459-18.214,42.582 c-6.831,28.891-2.677,56.027,11.999,79.226c-17.722,9.876-46.151,12.307-51.904,12.522H22.367 c-12.294,0.017-22.27,9.952-22.337,22.245c-0.549,41.234,6.437,82.222,20.614,120.946c16.214,42.521,40.336,73.842,71.719,93.01 c35.167,21.537,92.302,33.844,157.067,33.844c29.258,0.092,58.461-2.556,87.226-7.907c39.986-7.342,78.463-21.318,113.839-41.352 c29.149-16.88,55.383-38.354,77.688-63.596c37.29-42.213,59.505-89.226,76.026-131.007c2.215,0,4.431,0,6.584,0 c40.828,0,65.935-16.338,79.78-30.029c9.201-8.732,16.384-19.369,21.045-31.167l2.923-8.553L707.494,193.557z"/> <path d="M65.995,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.476-5.538 c-0.01,0-0.021,0-0.031,0H65.995c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C60.488,226.443,62.953,228.909,65.995,228.909L65.995,228.909"/> <path d="M152.913,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.059,0-5.538,2.479-5.538,5.538v56.181C147.392,226.448,149.866,228.909,152.913,228.909"/> <path d="M241.153,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C235.646,226.443,238.112,228.909,241.153,228.909L241.153,228.909"/> <path d="M328.348,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C322.841,226.443,325.307,228.909,328.348,228.909L328.348,228.909"/> <path d="M152.913,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.046,0-5.521,2.46-5.538,5.507v56.181C147.392,145.597,149.861,148.066,152.913,148.083"/> <path d="M241.153,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C235.646,145.591,238.107,148.066,241.153,148.083"/> <path d="M328.348,148.083h63.073c3.052-0.017,5.521-2.486,5.538-5.538V86.364c-0.017-3.047-2.491-5.507-5.538-5.507h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C322.841,145.591,325.302,148.066,328.348,148.083"/> <path d="M328.348,67.227h63.073c3.047,0,5.521-2.461,5.538-5.507V5.507C396.942,2.46,394.468,0,391.421,0h-63.073 c-3.042,0-5.507,2.465-5.507,5.507l0,0v56.212C322.841,64.761,325.307,67.227,328.348,67.227"/> <path d="M416.312,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.041,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C410.805,226.443,413.271,228.909,416.312,228.909"/> </svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

6
dist/index.html vendored
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
@@ -13,4 +13,6 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="vendor.css"><link rel="stylesheet" href="style.css"><meta name="viewport" content="width=device-width,initial-scale=1"><meta property="og:site_name" content="Docker Registry UI"><meta name="twitter:card" content="summary"><meta name="twitter:site" content="@Joxit"><meta name="twitter:creator" content="@Jones Magloire"><title>Docker Registry UI</title></head><body><app></app><script src="scripts/vendor.js"></script><script src="scripts/docker-registry-ui.js"></script></body></html>
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link href="docker-registry-ui.css" rel="stylesheet" type="text/css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta property="og:site_name" content="Docker Registry UI" /><meta name="twitter:card" content="summary" /><meta name="twitter:site" content="@Joxit" /><meta name="twitter:creator" content="@Jones Magloire" /><title>Docker Registry UI</title></head><body><docker-registry-ui registry-url="${REGISTRY_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><script src="docker-registry-ui.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

18
dist/style.css vendored

File diff suppressed because one or more lines are too long

1
dist/vendor.css vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 686 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,13 +1,13 @@
## Examples
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone)
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/master/examples/traefik)
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/master/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy)
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone)
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/main/examples/traefik)
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/main/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/main/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))

View File

@@ -18,16 +18,16 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.52",
"electron-is-dev": "^1.1.0",
"keytar": "^5.6.0",
"keytar": "^7.3.0",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"devDependencies": {
"copyfiles": "^2.2.0",
"electron": "^8.0.0",
"electron": "^11.2.3",
"electron-builder": "^22.6.0",
"electron-packager": "^14.2.1",
"electron-rebuild": "^1.10.1",
"electron-packager": "^15.2.0",
"electron-rebuild": "^2.3.5",
"parcel-bundler": "^1.12.4",
"typescript": "^3.8.3"
},

View File

@@ -24,14 +24,15 @@ services:
container_name: registry-srv
registry-ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
restart: always
ports:
- 80:80
environment:
- REGISTRY_TITLE=Private Docker Registry
- URL=http://localhost:5000
- REGISTRY_URL=http://localhost:5000
- DELETE_IMAGES=true
- SINGLE_REGISTRY=true
container_name: registry-ui
networks:

View File

@@ -1,6 +1,6 @@
# Example for issue #20 (HTTPS supports)
This example will override the original nginx conf with one supporting HTTPS. You will need to rewrite all the project configuration (replaces `proxy_pass` with our value).
This example will override the original nginx conf with one supporting HTTPS. You will need to rewrite all the project configuration (replaces `proxy_pass` with your own value, in this example `http://registry:5000` is fine).
Generating a self signed certificate:

View File

@@ -8,8 +8,9 @@ services:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
environment:
- SINGLE_REGISTRY=true
- REGISTRY_TITLE=My Private Docker Registry
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf

View File

@@ -6,6 +6,9 @@ server {
ssl_certificate_key /etc/nginx/certs/privkey.pem;
root /usr/share/nginx/html;
# disable any limits to avoid HTTP 413 for large image uploads
client_max_body_size 0;
location /v2 {
# Do not allow connections from docker 1.5 and earlier
# docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
@@ -19,6 +22,7 @@ server {
server {
listen 80;
location / {
# Force HTTPS
return 301 https://$host$request_uri;
}
}

View File

@@ -8,10 +8,11 @@ services:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
- NGINX_PROXY_PASS_URL=http://registry:5000
- SINGLE_REGISTRY=true
depends_on:
- registry
networks:

View File

@@ -10,12 +10,13 @@ services:
network_mode: host
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
ports:
- 80:80
environment:
- URL=http://127.0.0.1:5000
- REGISTRY_URL=http://127.0.0.1:5000
- DELETE_IMAGES=true
- SINGLE_REGISTRY=true
depends_on:
- registry
network_mode: host

View File

@@ -12,14 +12,15 @@ services:
container_name: registry-srv
registry-ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
restart: always
ports:
- 8080:80
environment:
- REGISTRY_TITLE=Private Docker Registry
- REGISTRY_URL=http://registry-srv:5000
- NGINX_PROXY_PASS_URL=http://registry-srv:5000
- DELETE_IMAGES=true
- SINGLE_REGISTRY=true
networks:
- registry-ui-net
container_name: registry-ui

View File

@@ -5,7 +5,7 @@ metadata:
labels:
app: registry
release: docker-registry-ui
app/version: "1.2.1"
app/version: "2.0.0"
spec:
replicas: 1
selector:

View File

@@ -4,7 +4,7 @@ metadata:
labels:
app: registry
release: docker-registry-ui
app/version: "1.2.1"
app/version: "2.0.0"
name: docker-registry
spec:
accessModes:

View File

@@ -5,7 +5,7 @@ metadata:
labels:
app: registry
release: docker-registry-ui
app/version: "1.2.1"
app/version: "2.0.0"
spec:
type: ClusterIP
ports:

View File

@@ -5,7 +5,7 @@ metadata:
labels:
app: registry-ui
release: docker-registry-ui
app/version: "1.2.1"
app/version: "2.0.0"
spec:
replicas: 1
selector:
@@ -27,10 +27,12 @@ spec:
value: "Docker registry UI"
- name: DELETE_IMAGES
value: "false"
- name: REGISTRY_URL
- name: NGINX_PROXY_PASS_URL
value: "http://docker-registry.default:5000"
- name: PULL_URL
value: "docker-registry-ui.default:80"
- name: SINGLE_REGISTRY
value: "true"
ports:
- name: http
containerPort: 80

View File

@@ -5,7 +5,7 @@ metadata:
labels:
app: registry-ui
release: docker-registry-ui
app/version: "1.2.1"
app/version: "2.0.0"
spec:
type: LoadBalancer
ports:

View File

@@ -10,13 +10,14 @@ services:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
- NGINX_PROXY_PASS_URL=http://registry:5000
- NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for
- SINGLE_REGISTRY=true
volumes:
- ./nginx.env:/etc/nginx/.env
depends_on:

View File

@@ -1,6 +1,6 @@
# Docker registry with read only access
This is the configuration for a docker registry UI using `REGISTRY_URL` (as a proxy) with read only access to the registry.
This example will override the original nginx conf with read only access to the registry. You will need to rewrite all the project configuration (replaces `proxy_pass` with your own value, in this example `http://registry:5000` is fine).
There are two htpasswd files. `read-write.htpasswd` a read and write access to the registry and `read-only.htpasswd` for a read only access.

View File

@@ -9,12 +9,12 @@ services:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
- SINGLE_REGISTRY=true
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./read-write.htpasswd:/etc/nginx/auth/read-write.htpasswd:ro

View File

@@ -0,0 +1,2 @@
data/registry/
data/keycloak/

View File

@@ -0,0 +1,137 @@
# Token Authentication with Keycloak
In this example, we'll see how to configure your keycloak server and use token authentication with your registry. This will use the [docker registry v2 token authentication protocol](https://docs.docker.com/registry/spec/auth/token/).
![token protocol](https://docs.docker.com/registry/spec/images/v2-registry-auth.png)
In this image, we will replace the docker client/daemon by the Docker Registry UI. Here are the steps:
1. Attempt to get a resource (catalog, image info, image delete) with the registry.
2. If the registry requires authorization it will return a `401 Unauthorized` HTTP response with information on how to authenticate.
3. The **docker registry ui** makes a request to **keycloak** for a Bearer token.
1. Your browser will use the [Basic Access Authentication Protocol](https://en.wikipedia.org/wiki/Basic_access_authentication#Protocol). But keycloak does not support this protocol... That's why we need a nginx proxy on top of keycloak.
2. Your proxy will receive a request on `/auth/realms/{realm name}/protocol/docker-v2/auth` without `Authentication` header. It will return a `401 Unauthorized` HTTP response with `WWW-Authenticate` header.
3. Your browser will ask you your credentials.
4. The proxy will pass the credentials to keycloak.
4. Keycloak returns an opaque Bearer token representing the clients authorized access.
5. The **docker registry ui** retries the original request with the Bearer token embedded in the requests Authorization header.
6. The Registry authorizes the client by validating the Bearer token and the claim set embedded within it and begins the session as usual.
:warning: If you are configuring from scratch your own keycloak server, remove files in `data` folder first with certificates in `conf/registry/localhost.*`
## Configure your nginx/proxy server
I will highlight required configuration for Basic Access Authentication Protocol. Replace the `{realm name}` by the name of your realm. In my example the realm is master, but you should create your own realm for your users.
```nginx
resolver 127.0.0.11 valid=30s;
set $keycloak "http://keycloak:8080";
# Location to get keycloak token
location /auth/realms/{realm name}/protocol/docker-v2/auth {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
# By default, keycloak returns 400 instead of 401, we need to change that
if ($http_authorization = "") {
add_header WWW-Authenticate 'Basic realm="Keycloak login"' always;
return 401;
}
proxy_pass $keycloak;
}
```
Start your nginx server. It will be available on http://localhost/ in my example.
```sh
docker-compose up -d proxy
```
## Configure your keycloak server
I will highlight required configuration for docker protocol. You will need to add this option to your keycloak command line:
```
-Dkeycloak.profile.feature.docker=enabled
```
Then the defalt user can be configured via environment variables
```yml
services:
keycloak:
image: jboss/keycloak
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: password
user: root
networks:
- registry-ui-net
command: -Dkeycloak.profile.feature.docker=enabled -b 0.0.0.0
```
Now you can start your keycloak server, it will be available on http://localhost/auth in my example.
```sh
docker-compose up -d keycloak
```
Now you need to configure your docker client with these steps:
Go to the keycloak home page: http://localhost/auth and click on `Administration Console`.
![keycloak-home](https://raw.github.com/Joxit/docker-registry-ui/main/examples/token-auth-keycloak/images/01-keycloak-home.png)
Sign in with your login and password (in my example it's `admin` and `password`).
![keycloak-home](https://raw.github.com/Joxit/docker-registry-ui/main/examples/token-auth-keycloak/images/02-keycloak-signin.png)
Go to `Clients` in the left side menu.
![keycloak-home](https://raw.github.com/Joxit/docker-registry-ui/main/examples/token-auth-keycloak/images/03-keycloak-to-clients.png)
Create a new client.
![keycloak-home](https://raw.github.com/Joxit/docker-registry-ui/main/examples/token-auth-keycloak/images/04-keycloak-create-client.png)
Enter a name for `Client ID`, choose `docker-v2` as the `Client Protocol`, and click `Save`.
![keycloak-home](https://raw.github.com/Joxit/docker-registry-ui/main/examples/token-auth-keycloak/images/05-keycloak-new-client.png)
Navigate to `Installation` tab, choose `Docker Compose YAML` as `Format Option` and click `Download`
![keycloak-home](https://raw.github.com/Joxit/docker-registry-ui/main/examples/token-auth-keycloak/images/06-keycloak-download.png)
When you extract the archive, the resulting directory should look like this.
```
keycloak-docker-compose-yaml
├── certs
│ ├── localhost.crt
│ ├── localhost.key
│ └── localhost_trust_chain.pem
├── data
├── docker-compose.yaml
└── README.md
```
Copy all the files from `certs` folder to `conf/registry` (this will replace files generated for this example).
## Configure your registry server
The last step is the configuration of your registry server. The config file is located in `conf/registry/config.yml`. The import part of the configuration is `auth.token` where you need to set `realm`, `service`, `issuer` and the `rootcertbundle` from the previous archive.
```yml
auth:
token:
realm: http://localhost/auth/realms/{realm name}/protocol/docker-v2/auth
service: docker-registry
issuer: http://localhost/auth/realms/{realm name}
rootcertbundle: /etc/docker/registry/localhost_trust_chain.pem
```
Now you can start your docker registry with your docker registry ui.
```sh
docker-compose up -d registry ui
```

View File

@@ -0,0 +1,78 @@
server {
listen 80;
server_name localhost;
resolver 127.0.0.11 valid=30s;
set $keycloak "http://keycloak:8080";
set $registry "http://registry:5000";
set $ui "http://ui";
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
# disable any limits to avoid HTTP 413 for large image uploads
client_max_body_size 0;
# required to avoid HTTP 411: see Issue #1486 (https://github.com/moby/moby/issues/1486)
chunked_transfer_encoding on;
# required for strict SNI checking: see Issue #70 (https://github.com/Joxit/docker-registry-ui/issues/70)
proxy_ssl_server_name on;
proxy_buffering off;
proxy_ignore_headers "X-Accel-Buffering";
location /v2 {
# Do not allow connections from docker 1.5 and earlier
# docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
return 404;
}
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_pass $registry;
}
location /auth {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_pass $keycloak;
}
location /auth/realms/master/protocol/docker-v2/auth {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
if ($http_authorization = "") {
add_header WWW-Authenticate 'Basic realm="Keycloak login"' always;
return 401;
}
proxy_pass $keycloak;
}
location /ui {
proxy_pass $ui;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

View File

@@ -0,0 +1,27 @@
version: 0.1
log:
fields:
service: registry
storage:
delete:
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['http://localhost']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization', 'Accept']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']
auth:
token:
realm: http://localhost/auth/realms/master/protocol/docker-v2/auth
service: docker-registry
issuer: http://localhost/auth/realms/master
rootcertbundle: /etc/docker/registry/localhost_trust_chain.pem

View File

@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICmzCCAYMCBgF3SzuJuTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0
ZXIwHhcNMjEwMTI4MjMwMDI5WhcNMzEwMTI4MjMwMjA5WjARMQ8wDQYDVQQDDAZt
YXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCliGzyYtDTTmaj
QYso8nxIY2tO5ITnRbgDQIXMuAY5HMv2XglT2zSHJNfC/HYSilZPSd8Ee/0oxm/q
On1Al3JENx21txUWOBe48CVzLlIYlUnIXqaFh0YyL6feUZaDKg1YSVGhSzHDI57X
DcfnR+g0V5QxzIKzK624Lw7vqGvZz5e9sS9mTn9EZUmqQRQBerB5qrPnuDLxEbj4
LPxqjuFyKQ4g8wooYlBNSFruRas3TpG/90Xy15pa9a3ofiVPZCt3IoaQGPw4Ah3O
ygnelgEhWNRwROx4dErmW2l7dUQP8dbSz+qI4g04Wx3GjnZAlY7mt7LG8OncavsA
Gp52m8QrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGvqPm9nmEMfGYjWN6JlnM2x
/YkKzvH6B+IeXz/j2bXKZqQoQ1up62HNM4e3GKSw/51Lge6QXqgOZmFSHABev8EV
+vzDMfRLjBfV1RmhZXYCh6nje0d61jAa0Sn6CfsUllIQRt3Hn67qzPk1d6SnKSHA
tsbh5+pCDivfJBRm7sJCv1y9dPP1rlaxAOZrVU8LEsJlTP3D0OScrDQv09CVonwi
4W2bnLcB6aPW5Fw3gyY4TtXfcQzQqbV5Gjs9EZNA6Vczu+80U14T4VD9CDgC8yky
2KY2pGClWEjM+dJnZ0440wXuGK/lJEN41SzfKxyfCBTJVebwOXsqsyonYYK8Pxo=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCliGzyYtDTTmaj
QYso8nxIY2tO5ITnRbgDQIXMuAY5HMv2XglT2zSHJNfC/HYSilZPSd8Ee/0oxm/q
On1Al3JENx21txUWOBe48CVzLlIYlUnIXqaFh0YyL6feUZaDKg1YSVGhSzHDI57X
DcfnR+g0V5QxzIKzK624Lw7vqGvZz5e9sS9mTn9EZUmqQRQBerB5qrPnuDLxEbj4
LPxqjuFyKQ4g8wooYlBNSFruRas3TpG/90Xy15pa9a3ofiVPZCt3IoaQGPw4Ah3O
ygnelgEhWNRwROx4dErmW2l7dUQP8dbSz+qI4g04Wx3GjnZAlY7mt7LG8OncavsA
Gp52m8QrAgMBAAECggEAS7VHztwvElXrT4Ost/+fpCQEckLGHlievq4GBAmunvRy
vK1pqra5IY5SOFXrUrN+oijxHUXwFXQcv44ctywNEPH8Xp3rwQvKncUH+9QVrDSr
WD8h/jROgKmXJ3E9W6QiEl0GPrT7f3qNLWKaKUeUBkx/9P7KUFQL0g+Dz0zCdw1k
wzJNE+tJgspeInlgylmXsuT2A0lPkIDq6uxZY/yhaEG03dytX12Mmg5VcfIzjLWB
cF0hKDbb+Pmu+tWbLHEwHg00Qj3k7Y21u9DrHRC0uBYrVsytSxDIuayRfwskAcIm
fyMNAYVtOQxzdaDm2OTT0SAZJOOxa+ZYMGToEzzFcQKBgQDTp961U0RAlbfygA4a
IDxSvLDaIQ1pZuDzHwm/8b7SNGWiTSCjSWXZFd35rOXian53u69ey61ZTIAyv1sQ
c7R1yCfyg8YUTWZLbl1s5Bb+ekh64WEaMmf+eChcegsXt/kW/wQKM9DLRMAM4v5Q
9g3VEMH5xuFdGRa2AwDVZrRB5wKBgQDINsNkPgxMq+B1GfnT6PpuyAWlb7GuUENc
yAucIBMGKdY8QlwnQmAaKPTl1t/MFsiiwUhRJiyXurG+skd3BmMPdACWnXa7nKBW
XoXM6MRhS1QrDds6hid1usO86fB50UhupSr3tkHdeWy8l1erll/rhrParSyer1iK
AjLfwx2rHQKBgQCaGTehpv0jVJ43tZoO1Xd1+aF9PuFH4zpWaDut/zEiVDnHAAaK
O+8mLbCOjp5UyZpITGKzTvFn+bXAvOdtRACYXGERRXWa5Htc4f6tQCepoZhRtvP+
ocJrWEpygfy/iReW8ZacYvtaczSsbTwh7/NENE42L+F26cRKQkeCF6OX8wKBgQDF
Y1hXp+SwYnO0f5uSlIryVTlb1TazyGXhP0hS8DxRQ0X3uuTnv8THhcGMJ8AUkhHU
hAIsHxqvrFw4ycMzUZSwU4mQ9EVuygg5no8DaijSU1Xz7IFKvaCBrVP1GB8BupdS
nnwyI/njxCaz9/FzNZnztqXy3fCzseP0jB5kBRVm8QKBgEDbzlXPdF5Jr4mb3+Pt
hqhAP1Swx2vwIyooDqhP0D5nUV1EDJ6Hqwb6L9R0Apt2gLW0YD1ETh9TsqUOWbIm
kiE9IDHZFbtBj014clx3if6snKarrjaHQJ9THzB5uwtv8b3/k81T/IyAnPmXobQx
Rrw+6hjfOdQsT90raYyhRWWx
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICmzCCAYMCBgF3SzrlgjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0
ZXIwHhcNMjEwMTI4MjI1OTQ3WhcNMzEwMTI4MjMwMTI3WjARMQ8wDQYDVQQDDAZt
YXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHFs8UdWQrDZ/K
RMvX/bu4SYa4fq38OHCu/ci8c/Vjuz8A4TI6hn84i82WLdHsiXpi4PO4YnGxTxHF
vI8cAarLW2aYv8ZK81YP63kPUMGS3dMtQE8CRTAtHBOVXrxp6ab18kTeQZ/KxiXy
nQM4rM7nSy+zoCY1AcPAvzLz0uqD/D8jBvsR90mrnHGrS2cJZCvaKkwfpMddUBOv
qnlrDjiv0T5PE0fShvZWlUJn4tVhjwXGv6Nu32o87BnEE+6LoQek8YqaXohQLE3V
MVFOHnxmCr7JwMk1Cdr+R2WyHleeC+FTL+O+kcpAzz9DIAhhUs2bHMR2OJAP3l4X
PfPPGeWhAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIy0ru1wK6HTqO2UrNxa/Yqa
GXdppS5tj2ssivKBqTc88TK2n8ORUyVVDeGVvicgASkru7ZjepR6APhdDG5Gy44x
xnj/207txc6YXz+TS/2JO5SMNWrwQCwmjT9Ld4HtjEzzxt3O7tGUmSpRIAoRo4Zy
gpaGuiHvJ0twhZWwcFS4sEYuGfF3uQ+hR2MQuSVm9El6GihY6c7dpv7E5GL71dDm
VPjuN9/rkxEUvFl0EHZByfUpqXnVhujEDgw8eSyOleIFNJ0vEKKJRnbwcIi2SQ2z
zPZjXKdr03R7YukmdMV5X7Swn7ehwF6AJijw6zpCoKcaiOQYexGMXxasK2xXw90=
-----END CERTIFICATE-----

View File

@@ -0,0 +1 @@
cc04bebe-cb07-4384-b886-3670f757fd2e

View File

@@ -0,0 +1,163 @@
2021-01-28 22:43:31 jdbc[3]: exception
org.h2.jdbc.JdbcSQLException: Table "DATABASECHANGELOGLOCK" not found; SQL statement:
select count(*) from PUBLIC.DATABASECHANGELOGLOCK [42102-197]
2021-01-28 22:43:32 jdbc[4]: exception
org.h2.jdbc.JdbcSQLException: Table "DATABASECHANGELOG" not found; SQL statement:
select count(*) from PUBLIC.DATABASECHANGELOG [42102-197]
2021-01-28 22:43:32 jdbc[4]: exception
org.h2.jdbc.JdbcSQLException: Table "DATABASECHANGELOG" not found; SQL statement:
select count(*) from PUBLIC.DATABASECHANGELOG [42102-197]
2021-01-28 22:44:05 jdbc[4]: exception
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
2021-01-28 22:44:05 jdbc[3]: exception
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
2021-01-28 23:16:37 jdbc[5]: exception
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
2021-01-28 23:16:37 jdbc[4]: exception
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
2021-01-28 23:16:37 jdbc[3]: exception
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
2021-01-28 23:17:00 jdbc[4]: exception
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
2021-01-28 23:17:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)

View File

@@ -0,0 +1,41 @@
version: '2.4'
services:
keycloak:
image: jboss/keycloak
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: password
user: root
volumes:
- ./data/keycloak:/opt/jboss/keycloak/standalone/data/
networks:
- registry-ui-net
command: -Dkeycloak.profile.feature.docker=enabled -b 0.0.0.0
proxy:
image: nginx:alpine
ports:
- 80:80
volumes:
- ./conf/proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- registry-ui-net
ui:
image: joxit/docker-registry-ui
environment:
REGISTRY_TITLE: My Private Docker Registry
REGISTRY_URL: http://localhost
SINGLE_REGISTRY: 'true'
networks:
- registry-ui-net
registry:
image: registry:2.7
volumes:
- ./conf/registry/:/etc/docker/registry/
- ./data/registry:/var/lib/registry
networks:
- registry-ui-net
networks:
registry-ui-net:

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -12,11 +12,12 @@ services:
constraints: [node.role == manager]
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
environment:
- DELETE_IMAGES=true
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://docker-registry_registry:5000
- NGINX_PROXY_PASS_URL=http://docker-registry_registry:5000
- SINGLE_REGISTRY=true
depends_on: ['registry']
networks: ['proxy', 'default']
deploy:

View File

@@ -10,12 +10,13 @@ services:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
- NGINX_PROXY_PASS_URL=http://registry:5000
- SINGLE_REGISTRY=true
depends_on:
- registry
networks:

View File

@@ -8,12 +8,13 @@ services:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
- NGINX_PROXY_PASS_URL=http://registry:5000
- SINGLE_REGISTRY=true
depends_on:
- registry
networks:

View File

@@ -13,7 +13,7 @@ docker-compose -f simple.yml up -d
./populate.sh
```
You can add some credentials to access your registry wit `credentials.yml` docker-compose file.
You can add some credentials to access your registry with `credentials.yml` docker-compose file.
Credentials for this example are login: `registry` and password: `ui` using bcrypt.
```sh

View File

@@ -10,11 +10,12 @@ services:
- ./registry-config/htpasswd:/etc/docker/registry/htpasswd
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- URL=http://localhost:5000
- REGISTRY_URL=http://localhost:5000
- SINGLE_REGISTRY=true
depends_on:
- registry

View File

@@ -9,11 +9,12 @@ services:
- ./registry-config/simple.yml:/etc/docker/registry/config.yml
ui:
image: joxit/docker-registry-ui:static
image: joxit/docker-registry-ui:latest
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- URL=http://localhost:5000
- REGISTRY_URL=http://localhost:5000
- SINGLE_REGISTRY=true
depends_on:
- registry

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View File

@@ -1,123 +0,0 @@
'use strict';
const cleanCSS = require('gulp-clean-css');
const concat = require('gulp-concat');
const del = require('del');
const filter = require('gulp-filter');
const gIf = require('gulp-if');
const gulp = require('gulp');
const parallel = gulp.parallel;
const series = gulp.series;
const htmlmin = require('gulp-htmlmin');
const license = require('gulp-license');
const riot = require('gulp-riot');
const uglify = require('uglify-es');
const minifier = require('gulp-uglify/composer')(uglify);
const useref = require('gulp-useref');
const injectVersion = require('gulp-inject-version');
const merge = require('stream-series');
const allTags = ['src/tags/*.riot', 'src/tags/dialogs/*.riot'];
const allScripts = [
'src/scripts/http.js',
'src/scripts/script.js',
'src/scripts/utils.js'
];
const staticTags = ['src/tags/*.riot'];
const staticScripts = [
'src/scripts/http.js',
'src/scripts/static.js',
'src/scripts/utils.js'
];
function html() {
var htmlFilter = filter('**/*.html', {restore: true});
return gulp.src(['src/index.html'])
.pipe(useref())
.pipe(gIf(['*.js', '!*.min.js'], minifier())) // FIXME
.pipe(htmlFilter)
.pipe(htmlmin({
removeComments: false,
collapseWhitespace: true,
removeRedundantAttributes: true,
removeEmptyAttributes: true,
minifyJS: uglify
}))
.pipe(htmlFilter.restore)
.pipe(gulp.dest('dist'));
};
function clean() {
return del(['dist']);
};
function appStatic() {
return merge(gulp.src(staticScripts), gulp.src(staticTags).pipe(riot()))
.pipe(concat('docker-registry-ui-static.js'))
.pipe(minifier())
.pipe(license('agpl3', {
tiny: false,
project: 'docker-registry-ui',
year: '2016-2019',
organization: 'Jones Magloire @Joxit'
}))
.pipe(injectVersion())
.pipe(gulp.dest('dist/scripts'));
};
function app() {
return merge(gulp.src(allScripts), gulp.src(allTags).pipe(riot()))
.pipe(concat('docker-registry-ui.js'))
.pipe(minifier())
.pipe(license('agpl3', {
tiny: false,
project: 'docker-registry-ui',
year: '2016-2019',
organization: 'Jones Magloire @Joxit'
}))
.pipe(injectVersion())
.pipe(gulp.dest('dist/scripts'));
};
function vendor() {
return gulp.src(['node_modules/riot/riot.min.js', 'node_modules/riot-route/dist/route.min.js', 'node_modules/riot-mui/build/js/riot-mui-min.js'])
.pipe(concat('vendor.js'))
.pipe(gulp.dest('dist/scripts'));
};
function styles() {
return gulp.src(['src/*.css'])
.pipe(concat('style.css'))
.pipe(cleanCSS({
compatibility: 'ie8'
}))
.pipe(license('agpl3', {
tiny: false,
project: 'docker-registry-ui',
year: '2016-2019',
organization: 'Jones Magloire @Joxit'
}))
.pipe(gulp.dest('dist/'));
};
function fonts() {
return gulp.src('src/fonts/*')
.pipe(filter('**/*.{otf,eot,svg,ttf,woff,woff2}'))
.pipe(gulp.dest('dist/fonts'));
};
function svgs() {
return gulp.src(['src/images/*.svg'])
.pipe(htmlmin({
removeComments: false,
collapseWhitespace: true,
removeRedundantAttributes: true,
removeEmptyAttributes: true,
minifyJS: uglify
}))
.pipe(gulp.dest('dist/images/'));
};
exports.build = series(clean, html, parallel(fonts, styles, vendor, app, appStatic, svgs));

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.3",
"version": "2.0.2",
"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#4d68d7f",
"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,85 @@
<!--
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"
if="{!props.filterResults || state.nImages > 0 || matchSearch(props.filterResults, state.image)}">
<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.nImages } images
<i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i>
</div>
</span>
</material-card>
<catalog-element if="{ state.images }" filter-results="{ props.filterResults }"
class="animated {!state.expanded && !props.filterResults ? 'hide' : ''} {state.expanding ? 'expanding' : ''}"
each="{item in state.images}" item="{ item }" />
</div>
<script>
import router from '../../scripts/router';
import {
matchSearch
} from '../search-bar.riot';
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;
state.nImages = props.item.images.length;
}
},
onBeforeUpdate(props, state) {
if (props.filterResults && state.images) {
state.nImages = state.images.filter(image => matchSearch(props.filterResults, image)).length;
} else {
state.nImages = state.images && state.images.length;
}
},
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;
},
matchSearch
}
</script>
<!-- End of tag -->
</catalog-element>

View File

@@ -0,0 +1,103 @@
<!--
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.nImages } images in { state.nRepositories } repositories</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 }" filter-results="{ props.filterResults }"/>
<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({
onAuthentication: this.props.onAuthentication
});
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,
nRepositories: repositories.length,
nImages: repositories.reduce((acc, e) => acc + (e.images && e.images.length || 1), 0),
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,108 @@
<!--
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/>.
-->
<confirm-delete-image>
<material-popup opened="{ props.opened }" onClick="{ props.onClick }">
<div slot="title">These images will be deleted</div>
<div slot="content">
<ul>
<li each="{ image in displayImagesToDelete(props.toDelete, props.tags) }">{ image.name }:{ image.tag }</li>
</ul>
</div>
<div slot="action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ deleteImages }">
Delete
</material-button>
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClick }">
Cancel
</material-button>
</div>
</material-popup>
<script>
import {
Http
} from '../../scripts/http';
import router from '../../scripts/router';
export default {
displayImagesToDelete(toDelete, tags) {
const digests = new Set();
toDelete.forEach(image => {
if (image.digest) {
digests.add(image.digest);
}
})
return tags.filter(image => digests.has(image.digest))
},
deleteImages() {
this.props.toDelete.forEach(image => this.deleteImage(image, this.props));
},
deleteImage(image, opts) {
const {
registryUrl,
ignoreError,
onNotify,
onAuthentication,
onClick
} = opts;
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({
onAuthentication: onAuthentication
});
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);
}
onClick();
});
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>
<style>
:host {
color: #000;
list-style-type: disc;
margin-block-start: 0.7em;
}
:host material-popup .content .material-popup-content {
overflow-y: auto;
max-height: 250px;
}
</style>
</confirm-delete-image>

View File

@@ -0,0 +1,128 @@
<!--
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>
<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>
<div class="container">
<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>
<div class="overlay" onclick="{ onClick }" if="{ state.isDropdownOpened }"></div>
<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 > .container{
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,62 @@
<!--
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 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,173 @@
<!--
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>
<search-bar on-search="{ onSearch }"></search-bar>
<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 }"
filter-results="{ state.filter }" on-authentication="{ onAuthentication }" />
</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="{ truthy(props.showContentDigest) }"
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
filter-results="{ state.filter }" on-authentication="{ onAuthentication }"></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="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
on-authentication="{ onAuthentication }"></tag-history>
</route>
</router>
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
on-close="{ onAuthenticationClose }" on-authenticated="{ state.onAuthenticated }"
opened="{ state.authenticationDialogOpened }"></registry-authentication>
<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 SearchBar from './search-bar.riot'
import {
stripHttps,
getRegistryServers,
truthy
} from '../scripts/utils';
import router from '../scripts/router';
export default {
components: {
Catalog,
TagList,
TagHistory,
DialogsMenu,
SearchBar,
Router,
Route
},
onUpdated(props, state) {
state.snackbarIsError = false;
state.snackbarMessage = undefined;
},
onBeforeMount(props) {
console.log(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 + '`.'
})
},
onAuthentication(opts, onAuthenticated) {
if (opts && opts.realm && opts.service && opts.scope) {
const {
realm,
service,
scope,
} = opts;
const req = new XMLHttpRequest()
req.addEventListener('loadend', () => {
try {
const bearer = JSON.parse(req.responseText);
onAuthenticated(bearer)
} catch (e) {
this.notifySnackbar(`Failed to log in: ${e.message}`, true)
}
})
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
req.send()
} else {
onAuthenticated()
}
},
onAuthenticationClose() {
this.update({
authenticationDialogOpened: false
})
},
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
});
}
},
onSearch(value) {
this.update({
filter: value
})
},
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
router,
version,
truthy
}
</script>
</docker-registry-ui>

View File

@@ -0,0 +1,48 @@
<search-bar>
<material-input placeholder="Search in page"></material-input>
<script>
import {
router
} from '@riotjs/route';
export default {
onMounted(props, state) {
const input = this.$('input');
let value = '';
const notify = () => {
if (value !== input.value) {
props.onSearch(input.value.toLowerCase())
}
value = input.value;
}
input.addEventListener('keyup', notify);
router.on.value(() => {
input.value = '';
notify();
})
}
}
export function matchSearch(search, value) {
return !search || (value && value.toLowerCase().indexOf(search) >= 0);
}
</script>
<style>
:host material-input {
position: absolute;
top: 0em;
right: 64px;
max-width: 20%;
}
@media screen and (max-width: 400px) {
:host material-input {
display: none;
}
}
:host material-input input {
color: #fff;
}
</style>
</search-bar>

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,192 @@
<!--
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, {
list: true,
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication
});
state.image.fillInfo()
},
onMounted(props, state) {
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,67 @@
<!--
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="{ getDockerCmd(props) }">
<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 {
onMounted(props, state) {
this.load(props, state);
},
onUpdated(props, state) {
this.load(props, state);
},
getDockerCmd(props) {
if (props.target === 'tag') {
return `docker pull ${props.pullUrl}/${props.image.name}:${props.image.tag}`;
} else {
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.digest}`
}
},
load(props, state) {
if (props.target !== 'tag' && !props.image.digest) {
props.image.one('content-digest', (digest) => {
this.update()
});
props.image.trigger('get-content-digest');
}
},
copy() {
const copyText = this.$('input');
if (!copyText.value) {
this.props.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
return;
}
copyText.style.display = 'block';
copyText.select();
document.execCommand('copy');
copyText.style.display = 'none';
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.')
}
}
</script>
</copy-to-clipboard>

View File

@@ -0,0 +1,60 @@
<!--
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="{ getTitle(props.image, state.chars) }">{ getDigest(props.image, state.chars) }</div>
<script>
export default {
onMounted(props, state) {
this.load(props, state);
},
onUpdated(props, state) {
this.load(props, state);
},
load(props, state) {
if (props.image.digest) {
return;
}
state.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.state.chars) {
this.update({
chars
});
}
},
getTitle(image, chars) {
return chars >= 70 ? '' : (image.digest || '');
},
getDigest(image, chars) {
if (chars >= 70) {
return image.digest || '';
} else if (chars <= 0) {
return '';
} else {
return image.digest && image.digest.slice(0, chars) + '...';
}
}
}
</script>
</image-content-digest>

View File

@@ -0,0 +1,41 @@
<!--
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-date>
<div title="Creation date { getLocalDate(props.image) }">{ getDate(props.image) } ago</div>
<script>
import {
dateFormat,
} from '../../scripts/utils';
export default {
onMounted(props) {
props.image.one('creation-date', (date) => {
this.update({
date: date,
localDate: date.toLocaleString()
});
});
props.image.trigger('get-date');
},
getDate(image) {
return dateFormat(image.creationDate)
},
getLocalDate(image) {
return (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
}
}
</script>
</image-date>

View File

@@ -0,0 +1,47 @@
<!--
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-size>
<div title="Compressed size of your image.">{ getImageSize(props.image) }</div>
<script>
import {
bytesToSize,
} from '../../scripts/utils';
export default {
onMounted(props, state) {
this.load(props, state);
},
onUpdated(props, state) {
this.load(props, state);
},
load(props, state) {
if (typeof props.image.size === 'number') {
return;
}
props.image.on('size', (size) => {
this.update({
size
});
});
props.image.trigger('get-size');
},
getImageSize(image) {
return bytesToSize(image.size)
}
}
</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,59 @@
<!--
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'
import {
ACTION_CHECK_TO_DELETE,
ACTION_UNCHECK_TO_DELETE,
ACTION_DELETE_IMAGE
} from './tag-table.riot';
export default {
onBeforeMount(props, state) {
state.checked = props.checked;
props.image.one('content-digest', (digest) => {
this.update({
digest
});
});
},
onMounted(props, state) {
props.image.trigger('get-content-digest');
},
onBeforeUpdate(props, state) {
state.checked = props.checked;
},
deleteImage() {
this.props.handleCheckboxChange(ACTION_DELETE_IMAGE, this.props.image);
},
handleCheckboxChange(checked) {
const action = checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
this.props.handleCheckboxChange(action, this.props.image);
}
}
</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,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/>.
-->
<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 }" filter-results="{ props.filterResults }"
on-authentication="{ props.onAuthentication }">
</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({
onAuthentication: props.onAuthentication
});
oReq.addEventListener('load', function () {
if (this.status == 200) {
const tags = (JSON.parse(this.responseText).tags || [])
.map(tag => new DockerImage(props.image, tag, {
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication
}))
.sort(compare);
window.requestAnimationFrame(self.onResize);
self.update({
page: Math.min(state.page, getNumPages(tags)),
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,216 @@
<!--
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>
<confirm-delete-image opened="{ state.confirmDeleteImage }" on-click="{ onConfirmDeleteImageClick }"
registry-url="{ props.registryUrl }" on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }"
tags="{ props.tags }" to-delete="{ state.toDelete }"></confirm-delete-image>
<material-card class="taglist">
<table style="border: none;">
<thead>
<tr>
<th
class="creation-date { (state.desc && state.orderType === 'date') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
onclick="{() => onPageReorder('date') }">
Creation date
</th>
<th
class="image-size { (state.desc && state.orderType === 'size') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
onclick="{() => onPageReorder('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="{ onReverseOrder }">Tag
</th>
<th class="show-tag-history">History</th>
<th class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
if="{ props.isImageRemoveActivated }">
<material-checkbox class="indeterminate" checked="{ state.multiDelete }"
if="{ state.toDelete.size === 0 || state.singleDeleteAction }"
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="{ deleteImages }"
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }">
<i class="material-icons">delete</i>
</material-button>
</th>
</tr>
</thead>
<tbody>
<tr each="{ image in getPage(props.tags, props.page) }" if="{ matchSearch(props.filterResults, image.tag) }">
<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 }" on-authentication="{ props.onAuthentication }" />
</td>
</tr>
</tbody>
</table>
</material-card>
<script>
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 from './remove-image.riot';
import {
matchSearch
} from '../search-bar.riot';
import ConfirmDeleteImage from '../dialogs/confirm-delete-image.riot';
const ACTION_CHECK_TO_DELETE = 'CHECK';
const ACTION_UNCHECK_TO_DELETE = 'UNCHECK';
const ACTION_DELETE_IMAGE = 'DELETE';
export default {
components: {
ImageDate,
ImageSize,
ImageTag,
ImageContentDigest,
CopyToClipboard,
RemoveImage,
TagHistoryButton,
ConfirmDeleteImage,
},
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
},
deleteImages() {
this.update({
confirmDeleteImage: true
})
},
onConfirmDeleteImageClick() {
if (this.state.singleDeleteAction) {
this.state.toDelete.clear();
}
this.update({
singleDeleteAction: false,
confirmDeleteImage: false
})
},
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(action, image) {
let confirmDeleteImage = false;
let singleDeleteAction = false;
switch (action) {
case ACTION_CHECK_TO_DELETE: {
this.state.toDelete.add(image);
break;
}
case ACTION_UNCHECK_TO_DELETE: {
this.state.toDelete.delete(image);
break;
}
case ACTION_DELETE_IMAGE: {
this.state.toDelete.clear();
this.state.toDelete.add(image);
confirmDeleteImage = true;
singleDeleteAction = true;
}
}
this.update({
toDelete: this.state.toDelete,
confirmDeleteImage,
singleDeleteAction
})
},
onReverseOrder() {
this.state.orderType = null;
this.state.desc = false;
this.props.onReverseOrder();
},
onPageReorder(type) {
this.update({
orderType: type,
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc
})
},
getPage(tags, page) {
const sortedTags = getPage(tags, page);
if (this.state.orderType === 'date') {
sortedTags.sort((e1, e2) =>
!this.state.desc ?
e2.creationDate.getTime() - e1.creationDate.getTime() :
e1.creationDate.getTime() - e2.creationDate.getTime());
} else if (this.state.orderType === 'size') {
sortedTags.sort((e1, e2) =>
!this.state.desc ?
e2.size - e1.size :
e1.size - e2.size);
}
return sortedTags;
},
matchSearch
}
export {
ACTION_CHECK_TO_DELETE,
ACTION_UNCHECK_TO_DELETE,
ACTION_DELETE_IMAGE
}
</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="${REGISTRY_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));
}

Some files were not shown because too many files have changed in this diff Show More