Compare commits

...

44 Commits
2.0.7 ... 2.3.1

Author SHA1 Message Date
Joxit
4e5b768833 fix(nginx): upgrading proxy_pass HTTP version from 1.0 to 1.1 (#270)
fixes #270
2022-10-08 01:03:13 +02:00
Joxit
c1f6c43e4a build: release 2.3.0 🚀 2022-09-20 22:29:20 +02:00
Joxit
fb8185907e fix(tag-list): missing details on images built by buildah (#264)
fixes #264
2022-09-19 08:44:09 +02:00
Joxit
636cb60ca8 docs(FAQ): improve issue about Basic auth and 401 status code (#266)
fixes #266
2022-09-18 14:20:05 +02:00
Jones Magloire
34fd13d6b7 feat(cache-control): new option USE_CONTROL_CACHE_HEADER adds Cache-Control header on requests to registry server (#265)
This option requires registry configuration: `Access-Control-Allow-Headers` with `Cache-Control`
2022-09-12 18:29:03 +02:00
Joxit
57a1cf919a fix(taglist): bug when listing multi-arch images (#260)
Support when registry server does not respect the [Manifest Specification](https://docs.docker.com/registry/spec/manifest-v2-2/#image-manifest-field-descriptions).
When the media type is `application/vnd.docker.distribution.manifest.list.v2+json` the response object must have a `layers` key, but sometimes you can have a `manifests` key...

fixes #260
2022-09-06 23:15:35 +02:00
Joxit
ad7c2698fd docs: announcement of the official helm chart and remove old example 2022-07-31 00:06:11 +02:00
Joxit
7032ec9f0f docs: add Artifact Hub badge 2022-07-22 19:32:54 +02:00
Joxit
3b9a804289 docs: fix typo in availible options NGINX_PROXY_PASS_HEADER_ 2022-07-11 08:50:57 +02:00
Hugo Cartigny
49ec1d9938 docs: update list of required headers to delete an image (#249) 2022-05-06 23:39:56 +02:00
Joxit
c84c9f36e6 fix(entrypoint): nginx crashes when overriding default.conf
fixes #247
closes #251
2022-05-06 23:26:02 +02:00
Joxit
1939b47677 docs(GA demo): use google analytics 4 2022-04-25 20:21:45 +02:00
Joxit
736d527cc8 docs(GA): use google analytics 4 2022-04-20 20:22:35 +02:00
Joxit
c310845c18 build: release 2.2.0 🚀 2022-04-18 14:57:13 +02:00
Alaa Attya Mohamed
f0c7232843 docs(helm): support ingress for k8s +1.19 and backward (#238) 2022-04-12 07:00:20 +02:00
Joxit
49fcba3f6c chore(cleanup): format code and move from js-beautify to prettier 2022-04-09 00:02:52 +02:00
Joxit
ab12cceefc chore: add support for windows development
closes #246
2022-04-08 23:24:35 +02:00
Joxit
3af4438815 docs: add missing documentation with sponsor link 2022-04-06 00:37:28 +02:00
Joxit
f826381681 fix(custom-labels): history fails on images with no labels 2022-04-03 00:02:15 +02:00
Joxit
ba2e0b119e chore: update dependencies 2022-04-02 11:15:07 +02:00
Joxit
772d19c18f ci: throw error when a file is missing (#242)
fixes #242
2022-04-02 10:59:49 +02:00
Joxit
ba6d817b41 feat: expose some custom labels 2022-03-23 09:18:20 +01:00
Jones Magloire
19e72e4a5f feat(catalog): show number of tags per image (#239) 2022-03-21 08:28:57 +01:00
Joxit
05cbb51125 feat: support OCI index images (such as produced by buildkit cache exports) (#227)
fixes #227
2022-03-19 22:35:41 +01:00
Joxit
7c0874694a fix: deleting cross-platform images leaves the deleted image in an unknown state (#226)
closes #226
2022-03-13 10:51:05 +01:00
Joxit
126509d7fa fix: wrong registry url when index.html is present (#225)
fixes #225
2022-03-11 19:56:26 +01:00
Jones Magloire
e1fd515279 feat: add support for unprivileged user (#234)
closes #224
2022-03-09 20:41:43 +01:00
Daniel Abbatt
befbd0bcfb docs(kubernetes): fix typo in README.md (#228) 2022-01-25 09:52:06 +01:00
Joxit
8ddfb1b5ae build: release 2.1.0 🚀 2021-11-09 23:07:12 +01:00
Joxit
3a385fc08d feat(nginx): add support for proxy_pass_header directive via NGINX_PROXY_PASS_HEADER_*
fixes #206
2021-11-08 05:28:39 +01:00
Joxit
f958365336 feat(search bar): add shortcuts CRTL + F or F3 to select the search bar
fixes #213
2021-11-06 14:50:18 +01:00
Joxit
29c17b1baa docs: add documentation + example for DEFAULT_REGISTRIES and READ_ONLY_REGISTRIES 2021-11-05 22:39:19 +01:00
Joxit
b323dc6c04 feat: add new option READ_ONLY_REGISTRIES available when SINGLE_REGISTRY=false 2021-11-04 00:48:33 +01:00
Joxit
992328eae9 fix: should update the catalog when the server is changed 2021-11-02 22:51:32 +01:00
Joxit
dd26bf66a2 feat: default registries is set only when there is no registries 2021-11-01 10:55:55 +01:00
Joxit
8fcae3cda4 feat: add option for default registries when SINGLE_REGISTRY=false 2021-10-31 15:09:37 +01:00
Joxit
f4455703ca docs: default value for SHOW_CONTENT_DIGEST is false since 2.0.0 2021-10-30 18:01:23 +02:00
Joxit
22e3f2254e docs: more issues for DELETE and CORS
fixes #207
fixes #214
2021-10-30 18:01:23 +02:00
Max H. Gerlach
4075e0005c docs: fix directory in electron install instructions (#208) 2021-10-16 08:57:12 +02:00
John Poth
7c00b85183 chore: update electron example (#210)
Add examples to electron path
Add run command
2021-10-16 08:40:09 +02:00
Joxit
21e3ad51af fix(warn): Refused to get unsafe header "www-authenticate" 2021-09-13 09:14:00 +02:00
Joxit
73613a3b96 fix: The tag seo is not a recognized Liquid tag. 2021-09-11 15:52:51 +02:00
Joxit
6da744a9c5 ci: add major and patch tag versions 2021-08-25 07:48:41 +02:00
Joxit
db6b74a5f0 fix(docker-image): minor typo for sha256
fixes #201
2021-07-13 23:32:57 +02:00
64 changed files with 1133 additions and 1294 deletions

View File

@@ -1,4 +1,4 @@
name: Build and pus master/main docker images
name: Build and push master/main docker images
on:
push:

View File

@@ -15,9 +15,15 @@ jobs:
run: npm install
- name: Build the interface
run: npm run build
- name: Current tag
id: current-tag
- name: Major tag
id: major-tag
run: echo "::set-output name=tag::$(git describe --tags | grep -o '^[0-9]*')"
- name: Minor tag
id: minor-tag
run: echo "::set-output name=tag::$(git describe --tags | grep -o '^[0-9]*\.[0-9]*')"
- name: Patch tag
id: patch-tag
run: echo "::set-output name=tag::$(git describe --tags | grep -o '^[0-9]*\.[0-9]*\.[0-9]*')"
- name: Download kokai
run: curl -sSL https://github.com/Joxit/kokai/releases/download/$(curl -sSL https://api.github.com/repos/Joxit/kokai/releases/latest | jq -r ".tag_name")/kokai-linux-x86_64 > kokai
- name: Create Release Note
@@ -51,7 +57,9 @@ jobs:
push: true
tags: |
joxit/docker-registry-ui:latest
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}
joxit/docker-registry-ui:${{steps.major-tag.outputs.tag}}
joxit/docker-registry-ui:${{steps.minor-tag.outputs.tag}}
joxit/docker-registry-ui:${{steps.patch-tag.outputs.tag}}
- name: Build and push Latest Debian Version
uses: docker/build-push-action@v2
with:
@@ -61,4 +69,6 @@ jobs:
push: true
tags: |
joxit/docker-registry-ui:debian
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}-debian
joxit/docker-registry-ui:${{steps.major-tag.outputs.tag}}-debian
joxit/docker-registry-ui:${{steps.minor-tag.outputs.tag}}-debian
joxit/docker-registry-ui:${{steps.patch-tag.outputs.tag}}-debian

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"singleQuote": true,
"tabWidth": 2,
"quoteProps": "preserve",
"printWidth": 120,
"proseWrap": "preserve"
}

View File

@@ -34,4 +34,9 @@
- Sepp Zuther [@Herr-Sepp](https://github.com/Herr-Sepp)
- Tomas Hulata [@tombokombo](https://github.com/tombokombo)
- Ben Jackson [@bjj](https://github.com/bjj)
- 三十文 [@xfduan](https://github.com/xfduan)
- 三十文 [@xfduan](https://github.com/xfduan)
- Aram Akhavan [@kaysond](https://github.com/kaysond)
- Jason Tackaberry [@jtackaberry](https://github.com/jtackaberry)
- Maxime Loliée [@loliee](https://github.com/loliee)
- Enrico [@Enrico204](https://github.com/Enrico204)
- [@clyvari](https://github.com/clyvari)

View File

@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
ENV NGINX_LISTEN_PORT '80'
ENV SHOW_CATALOG_NB_TAGS 'false'
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx

View File

@@ -6,6 +6,8 @@ title: Docker Registry User Interface
![Stars](https://img.shields.io/github/stars/joxit/docker-registry-ui.svg?logo=github&maxAge=86400)
![Pulls](https://img.shields.io/docker/pulls/joxit/docker-registry-ui.svg?maxAge=86400)
[![Sponsor](https://joxit.dev/images/sponsor.svg)](https://github.com/sponsors/Joxit)
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/joxit)](https://artifacthub.io/packages/search?repo=joxit)
## Overview
@@ -15,10 +17,12 @@ You may need the [migration guide from 1.x to 2.x](https://github.com/Joxit/dock
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/main/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), [Helm Chart](https://helm.joxit.dev/)
![preview](https://raw.github.com/Joxit/docker-registry-ui/main/docker-registry-ui.gif "Preview of Docker Registry UI")
If you like my work and want to support it, don't hesitate to [sponsor me](https://github.com/sponsors/Joxit).
## Features
- List all your repositories/images.
@@ -41,9 +45,17 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- 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)).
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `false`) (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))
- Set a list of default registries with `DEFAULT_REGISTRIES` (see [#219](https://github.com/Joxit/docker-registry-ui/pull/219)).
- Desactivate add and remove regisitries with `READ_ONLY_REGISTRIES` (see [#219](https://github.com/Joxit/docker-registry-ui/pull/219)).
- Filter images and tags with a search bar. You can select the search bar with the shortcut `CRTL` + `F` or `F3`. When the search bar is already focused, the shortcut will fallback to the default behavior (see [#213](https://github.com/Joxit/docker-registry-ui/issues/213)).
- **Forward** custom header to your backend registry via environment variable and file via `NGINX_PROXY_PASS_HEADER_*` (see [#206](https://github.com/Joxit/docker-registry-ui/pull/206)).
- Run the container with user nginx instead of root via `--user nginx` and listend on custom port via `NGINX_LISTEN_PORT` (see [#224](https://github.com/Joxit/docker-registry-ui/issues/224)).
- Show number of tags per images on catalog page. This will produce + nb images requests, not recommended on large registries via `SHOW_CATALOG_NB_TAGS` (default: `false`) (see [#161](https://github.com/Joxit/docker-registry-ui/issues/161) and [#239](https://github.com/Joxit/docker-registry-ui/pull/239)).
- Expose custom labels in history page, custom labels will be processed like maintainer label via `HISTORY_CUSTOM_LABELS` (see [#160](https://github.com/Joxit/docker-registry-ui/issues/160) and [#240](https://github.com/Joxit/docker-registry-ui/pull/240)).
- Access to the official Helm Chart: https://helm.joxit.dev/
## FAQ
@@ -63,31 +75,42 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- This means you are using a UI with HTTPS and your registry is using HTTP (unsecured). When you are on a HTTPS site, you can't get HTTP content. Upgrade you registry with a HTTPS connection.
- Why the default nginx `Host` is set to `$http_host` ?
- This fixes the issue [#88](https://github.com/Joxit/docker-registry-ui/issues/88). More about this in [#113](https://github.com/Joxit/docker-registry-ui/issues/113).
- 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)).
- Why OPTIONS (aka preflight requests) and DELETE fails with 401 status code (using Basic Auth) ?
- This is caused by a bug in docker registry, it returns 401 status requests on preflight requests, this breaks [W3C preflight-request specification](https://www.w3.org/TR/cors/#preflight-request). I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/ **or** use `NGINX_PROXY_PASS_URL` **or** configure a nginx/apache/haproxy in front of your registry that returns 200 on each OPTIONS requests. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104), [#204](https://github.com/Joxit/docker-registry-ui/issues/204), [#207](https://github.com/Joxit/docker-registry-ui/issues/207), [#214](https://github.com/Joxit/docker-registry-ui/issues/214), [#266](https://github.com/Joxit/docker-registry-ui/issues/266)).
- 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/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.
- Can I run the container with an unprivileged user ?
- Yes you can run the container with the `nginx` user, (see [#224](https://github.com/Joxit/docker-registry-ui/issues/224)).
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples) or open an issue.
## Available options
You can run the container with the unprivileged user `nginx`, see the discussion [#224](https://github.com/Joxit/docker-registry-ui/issues/224).
Some env options are available for use this interface for **only one server**.
- `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`)
- `SHOW_CONTENT_DIGEST`: Show content digest in docker tag list. (default: `false`)
- `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`)
- `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.
- `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.
- `NGINX_PROXY_PASS_HEADER_*`: Update the default Nginx configuration and **forward custom headers** to your backend docker registry. Only when `NGINX_PROXY_PASS_URL` is used.
- `NGINX_LISTEN_PORT`: Listen on a port other than 80. (default: `80` when the user is root, `8080` otherwise).
- `DEFAULT_REGISTRIES`: List of comma separated registry URLs (e.g `http://registry.example.com,http://registry:5000`), available only when `SINGLE_REGISTRY=false`. (default: ` `).
- `READ_ONLY_REGISTRIES`: Desactivate dialog for remove and add new registries, available only when `SINGLE_REGISTRY=false`. (default: `false`).
- `SHOW_CATALOG_NB_TAGS`: Show number of tags per images on catalog page. This will produce + nb images requests, not recommended on large registries. (default: `false`).
- `HISTORY_CUSTOM_LABELS`: Expose custom labels in history page, custom labels will be processed like maintainer label.
- `USE_CONTROL_CACHE_HEADER`: Use `Control-Cache` header and set to `no-store, no-cache`. This will avoid some issues on multi-arch images (see [#260](https://github.com/Joxit/docker-registry-ui/issues/260)). This option requires registry configuration: `Access-Control-Allow-Headers` with `Cache-Control`. (default: `false`).
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/).
@@ -106,7 +129,7 @@ http:
headers:
Access-Control-Allow-Origin: ['http://registry.example.com']
Access-Control-Allow-Credentials: [true]
Access-Control-Allow-Headers: ['Authorization', 'Accept']
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS'] # Optional
```
@@ -128,6 +151,7 @@ And you need to add these HEADERS:
http:
headers:
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
Access-Control-Expose-Headers: ['Docker-Content-Digest']
```
@@ -155,7 +179,7 @@ http:
X-Content-Type-Options: [nosniff]
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-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']
@@ -182,3 +206,4 @@ check out the [Electron](examples/electron/README.md) standalone application.
- [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))
- [Use DEFAULT_REGISTRIES and READ_ONLY_REGISTRIES](https://github.com/Joxit/docker-registry-ui/tree/main/examples/pr-219) ([#219](https://github.com/Joxit/docker-registry-ui/issues/219))

View File

@@ -1,7 +1,7 @@
title: Docker Registry User Interface
description: The simplest and most complete UI for your private registry!
url: https://joxit.dev/docker-registry-ui
google_analytics: UA-99119327-1
google_analytics: G-T158HYBVZ2
remote_theme: joxit/joxit.github.io
author: Jones Magloire
twitter:
@@ -12,4 +12,6 @@ defaults:
- scope:
path: ""
values:
image: /screenshot.png
image: /screenshot.png
plugins:
- jekyll-seo-tag

View File

@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
ENV NGINX_LISTEN_PORT '80'
ENV SHOW_CATALOG_NB_TAGS 'false'
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx

View File

@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
ENV NGINX_LISTEN_PORT '80'
ENV SHOW_CATALOG_NB_TAGS 'false'
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx

71
bin/90-docker-registry-ui.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/sh
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
sed -i "s~\${DEFAULT_REGISTRIES}~${DEFAULT_REGISTRIES}~" index.html
sed -i "s~\${READ_ONLY_REGISTRIES}~${READ_ONLY_REGISTRIES}~" index.html
sed -i "s~\${SHOW_CATALOG_NB_TAGS}~${SHOW_CATALOG_NB_TAGS}~" index.html
sed -i "s~\${HISTORY_CUSTOM_LABELS}~${HISTORY_CUSTOM_LABELS}~" index.html
sed -i "s~\${USE_CONTROL_CACHE_HEADER}~${USE_CONTROL_CACHE_HEADER}~" index.html
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i "s/\${DELETE_IMAGES}/false/" index.html
else
sed -i "s/\${DELETE_IMAGES}/true/" index.html
fi
get_nginx_proxy_headers() {
(
env &&
if [ -f "/etc/nginx/.env" ]; then
cat /etc/nginx/.env
# Force new line
echo ""
fi
) | while read e; do
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_HEADER_')" ]; then
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_HEADER_//' | sed 's/_/-/g')
value=${e#*=}
echo -n "proxy_set_header ${key} \"${value}\"; "
fi
done
}
get_nginx_proxy_pass_headers() {
(
env &&
if [ -f "/etc/nginx/.env" ]; then
cat /etc/nginx/.env
# Force new line
echo ""
fi
) | while read e; do
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_PASS_HEADER_')" ]; then
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_PASS_HEADER_//' | sed 's/_/-/g')
echo -n "proxy_pass_header \"${key}\"; "
fi
done
}
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^\${NGINX_PROXY_PASS_HEADERS}^$(get_nginx_proxy_pass_headers)^" /etc/nginx/conf.d/default.conf
sed -i "s,#!,," /etc/nginx/conf.d/default.conf
fi
if [ "$(whoami)" != "root" ]; then
if [ "$NGINX_LISTEN_PORT" = "80" ]; then
NGINX_LISTEN_PORT="8080"
fi
sed -i "/user nginx;/d" /etc/nginx/nginx.conf
sed -i "s,/var/run/nginx.pid,/tmp/nginx.pid," /etc/nginx/nginx.conf
fi
if [ "$NGINX_LISTEN_PORT" != "80" ]; then
sed -i "s,listen 80;,listen $NGINX_LISTEN_PORT;," /etc/nginx/conf.d/default.conf
fi

View File

@@ -1,37 +0,0 @@
#!/bin/sh
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 "s/\${DELETE_IMAGES}/false/" index.html
else
sed -i "s/\${DELETE_IMAGES}/true/" index.html
fi
get_nginx_proxy_headers() {
(
env &&
if [ -f "/etc/nginx/.env" ]; then
cat /etc/nginx/.env
# Force new line
echo ""
fi
) | while read e; do
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_HEADER_')" ]; then
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_HEADER_//' | sed 's/_/-/g')
value=${e#*=}
echo -n "proxy_set_header ${key} \"${value}\"; "
fi
done
}
if [ -n "${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

@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
WORKDIR /usr/share/nginx/html/
ENV NGINX_PROXY_HEADER_Host '$http_host'
ENV NGINX_LISTEN_PORT '80'
ENV SHOW_CATALOG_NB_TAGS 'false'
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
COPY dist/ /usr/share/nginx/html/
COPY favicon.ico /usr/share/nginx/html/
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx

View File

@@ -37,7 +37,7 @@
<body>
<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">
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false" default-registries="https://joxit.dev/docker-registry-demo">
<script>
if (localStorage.getItem('registryServer')) {
localStorage.setItem('registryServer',
@@ -46,10 +46,6 @@
'https://joxit.dev/docker-registry-demo'
)
)
} else {
localStorage.setItem('registryServer', JSON.stringify([
'https://joxit.dev/docker-registry-demo'
]))
}
</script>
<script src="../dist/docker-registry-ui.js"></script>
@@ -66,7 +62,7 @@
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-99119327-1', 'auto');
ga('create', 'G-T158HYBVZ2', 'auto');
ga('send', 'pageview');
</script>
</body>

File diff suppressed because one or more lines are too long

17
dist/index.html vendored
View File

@@ -13,6 +13,17 @@
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 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>
--><!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}"
default-registries="${DEFAULT_REGISTRIES}"
read-only-registries="${READ_ONLY_REGISTRIES}"
show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"
history-custom-labels="${HISTORY_CUSTOM_LABELS}"
use-control-cache-header="${USE_CONTROL_CACHE_HEADER}"
></docker-registry-ui><script src="docker-registry-ui.js"></script></body></html>

View File

@@ -10,4 +10,5 @@
- [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))
- [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))
- [Use DEFAULT_REGISTRIES and READ_ONLY_REGISTRIES](https://github.com/Joxit/docker-registry-ui/tree/main/examples/pr-219) ([#219](https://github.com/Joxit/docker-registry-ui/issues/219))

View File

@@ -16,10 +16,14 @@ computer.
* After building the web application, navigate to the ```electron``` directory
and execute following commands to build the executable:
```bash
cd electron
cd examples/electron
npm install
npm run dist
```
* Run the application:
```bash
npm start
```
If you encounter any issues, please check the troubleshooting below.

View File

@@ -1,22 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -1,11 +0,0 @@
apiVersion: v1
appVersion: "1.2.1"
description: The simplest and most complete UI for your private registry
name: docker-registry-ui
home: https://github.com/Joxit/docker-registry-ui
keywords:
- docker
- registry
sources:
- https://github.com/Joxit/docker-registry-ui
version: 0.1.0

View File

@@ -1,97 +1,23 @@
# docker-registry-ui
[docker-registry-ui](https://joxit.dev/docker-registry-ui/) is the simplest and most complete UI for your private registry!
:warning: The official helm chart is now located at https://helm.joxit.dev and on GitHub [github.com/Joxit/helm-charts](https://github.com/Joxit/helm-charts).
## Usage
## TL;DR;
1. Add my Helm repository (named `joxit`)
```bash
$ helm install .
```
helm repo add joxit https://helm.joxit.dev
```
## Introduction
2. Ensure you have access to the Helm chart and you see the latest chart version listed. If you have previously added the Helm repository, run `helm repo update`.
This chart bootstraps a [docker-registry-ui](https://joxit.dev/docker-registry-ui/) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
It also may deploy the [docker registry](https://docs.docker.com/registry/) if you havent have one already.
## Prerequisites
- Kubernetes 1.9+ with Beta APIs enabled
- PV provisioner support in the underlying infrastructure
## Installing the Chart
To install the chart with the release name `my-release`:
```bash
$ helm update --install my-release .
```
helm search repo joxit/docker-registry-ui
```
The command deploys docker-registry-ui on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.
3. Now you're ready to install the Docker Registry UI! To install Docker Registry UI with the default configuration using Helm 3.2 run the following command below. This will deploy the Docker Registry UI on the default namespace.
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart
To uninstall/delete the `my-release` deployment:
```bash
$ helm delete my-release
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
## Configuration
The following table lists the configurable parameters of the Redmine chart and their default values.
| Parameter | Description | Default |
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------- |
| `ui.title` | Title of the managed repository | `Docker registry UI` |
| `ui.delete_images` | Allow to delete image from the front-end | `false` |
| `ui.proxy` | The UI service act as a proxy of the registry | `true` |
| `ui.replicaCount` | Number of replicas to start | `1` |
| `ui.image.registry` | registry to pull the docker-registry-ui image from | `docker.io` |
| `ui.image.repository` | docker-registry-ui image name | `joxit/docker-registry-ui` |
| `ui.image.tag` | docker-registry-ui image tag (change to latest to have multi registry support) | `static` |
| `ui.image.pullPolicy` | docker-registry-ui image pull policy | `Always` |
| `ui.probe.liveness` | Ask kubernetes to check the service port for liveness | `true` |
| `ui.probe.readyness ` | Ask kubernetes to check the service port for readyness | `true` |
| `ui.service.type` | Desired service type | `ClusterIP` |
| `ui.service.port` | Service exposed port | `80` |
| `ui.ingress.enabled` | Create an ingress for docker-regstry-ui | `false` |
| `registry.external` | Use an already available registry | `false` |
| `registry.url` | URL of the existing registry | `http://localhost:5000` |
| `registry.replicaCount` | Number of replicas to start | `1` |
| `registry.image.registry` | registry to pull the docker-registry image from | `docker.io` |
| `registry.image.repository` | docker-registry-ui image name | `registry` |
| `registry.image.tag` | docker-registry-ui image tag | `2.6.2` |
| `registry.image.pullPolicy` | docker-registry-ui image pull policy | `Always` |
| `registry.probe.liveness` | Ask kubernetes to check the service port for liveness | `true` |
| `registry.probe.readyness ` | Ask kubernetes to check the service port for readyness | `true` |
| `registry.persistence.enabled` | Enable persistence using PVC for the registry | `false` |
| `registry.persistence.storageClass` | PVC Storage Class | `-` |
| `registry.persistence.size` | PVC Storage Request size | `1Gi` |
| `registry.service.type` | Desired service type | `ClusterIP` |
| `registry.service.port` | Service exposed port | `5000` |
| `registry.ingress.enabled` | Create an ingress for the regstry | `false` |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,
```bash
$ helm upgrade --install my-release \
--set registry.external=true \
--set registry.url=http://registry.example.com:5000 \
.
helm upgrade --install docker-registry-ui joxit/docker-registry-ui
```
Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example,
```bash
$ helm upgrade --install my-release -f values.yaml .
```
> **Tip**: You can use the default [values.yaml](values.yaml)

View File

@@ -1,147 +0,0 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "docker-registry-ui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "docker-registry-ui.fullname" -}}
{{- if .Values.ui.fullnameOverride -}}
{{- .Values.ui.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- printf "%s-ui" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-ui-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "docker-registry.fullname" -}}
{{- if .Values.registry.fullnameOverride -}}
{{- .Values.registry.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- printf "%s-registry" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-registry-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "docker-registry-ui.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "docker-registry-ui.labels" -}}
app: registry-ui
chart: {{ include "docker-registry-ui.chart" . }}
release: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- end -}}
{{- define "docker-registry-ui.matchLabels" -}}
app: registry-ui
release: {{ .Release.Name }}
{{- end -}}
{{- define "docker-registry.labels" -}}
app: registry
chart: {{ include "docker-registry-ui.chart" . }}
release: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- end -}}
{{- define "docker-registry.matchLabels" -}}
app: registry
release: {{ .Release.Name }}
{{- end -}}
{{- define "docker-registry-ui.probes" -}}
{{- if and .Values.ui.probe.liveness (eq .Values.ui.probe.liveness true) -}}
livenessProbe:
httpGet:
path: /
port: http
{{- end -}}
{{- if and .Values.ui.probe.readiness (eq .Values.ui.probe.readiness true) }}
readinessProbe:
httpGet:
path: /
port: http
{{- end -}}
{{- end -}}
{{- define "docker-registry.probes" -}}
{{- if and .Values.registry.probe.liveness (eq .Values.registry.probe.liveness true) -}}
livenessProbe:
httpGet:
path: /v2/
port: registry
{{- end -}}
{{- if and .Values.registry.probe.readiness (eq .Values.registry.probe.readiness true) }}
readinessProbe:
httpGet:
path: /v2/
port: registry
{{- end -}}
{{- end -}}
{{- define "docker-registry-ui.url-name" -}}
{{- if eq .Values.ui.proxy true -}}
REGISTRY_URL
{{- else -}}
URL
{{- end -}}
{{- end -}}
{{- define "docker-registry-ui.url-value" -}}
{{- if eq .Values.registry.external true -}}
{{ .Values.registry.url }}
{{- else -}}
{{- $fullName := include "docker-registry.fullname" . -}}
{{ printf "http://%s.%s:%.0f" $fullName .Release.Namespace .Values.registry.service.port }}
{{- end -}}
{{- end -}}
{{- define "docker-registry-ui.pull" -}}
{{- if eq .Values.registry.external true -}}
{{ .Values.registry.url }}
{{- else -}}
{{- if eq .Values.ui.proxy true -}}
{{- if eq .Values.ui.ingress.enabled true -}}
{{- $host := index .Values.ui.ingress.hosts 0 -}}
{{ $host.host }}
{{- else -}}
{{- $fullName := include "docker-registry-ui.fullname" . -}}
{{ printf "%s.%s:%.0f" $fullName .Release.Namespace .Values.ui.service.port }}
{{- end -}}
{{- else -}}
{{- if eq .Values.registry.ingress.enabled true -}}
{{- $host := index .Values.registry.ingress.hosts 0 -}}
{{ $host.host }}
{{- else -}}
{{- $fullName := include "docker-registry.fullname" . -}}
{{ printf "%s.%s:%.0f" $fullName .Release.Namespace .Values.registry.service.port }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@@ -1,31 +0,0 @@
{{- if eq .Values.registry.external false -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "docker-registry.fullname" . }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
data:
config.yml: |-
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: ['*']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']
{{- end -}}

View File

@@ -1,62 +0,0 @@
{{- if eq .Values.registry.external false -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "docker-registry.fullname" . }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
spec:
replicas: {{ .Values.registry.replicaCount }}
selector:
matchLabels:
{{ include "docker-registry.matchLabels" . | indent 6 }}
template:
metadata:
labels:
{{ include "docker-registry.matchLabels" . | indent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: config
configMap:
defaultMode: 420
name: {{ include "docker-registry.fullname" . }}
- name: data
{{- if .Values.registry.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "docker-registry.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
containers:
- name: registry
image: "{{ .Values.registry.image.registry }}/{{ .Values.registry.image.repository }}:{{ .Values.registry.image.tag }}"
imagePullPolicy: {{ .Values.registry.image.pullPolicy }}
ports:
- name: registry
containerPort: 5000
protocol: TCP
volumeMounts:
- mountPath: "/var/lib/registry"
name: "data"
- mountPath: "/etc/docker/registry"
name: "config"
{{ include "docker-registry.probes" . | indent 10 }}
resources:
{{- toYaml .Values.registry.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end -}}

View File

@@ -1,34 +0,0 @@
{{- if and (eq .Values.registry.external false) (and (eq .Values.ui.proxy false) .Values.registry.ingress.enabled) -}}
{{- $fullName := include "docker-registry.fullname" . -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
{{- with .Values.registry.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.registry.ingress.tls }}
tls:
{{- range .Values.registry.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.registry.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
- path: /
backend:
serviceName: {{ $fullName }}
servicePort: registry
{{- end }}
{{- end -}}

View File

@@ -1,23 +0,0 @@
{{- if and (eq .Values.registry.external false) .Values.registry.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
{{ include "docker-registry.labels" . | indent 4 }}
name: {{ include "docker-registry.fullname" . }}
spec:
accessModes:
{{- range .Values.registry.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.registry.persistence.size }}
{{- if .Values.registry.persistence.storageClass }}
{{- if (eq "-" .Values.registry.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.registry.persistence.storageClass | quote }}
{{- end }}
{{- end }}
{{- end -}}

View File

@@ -1,17 +0,0 @@
{{- if eq .Values.registry.external false -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "docker-registry.fullname" . }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
spec:
type: {{ .Values.registry.service.type }}
ports:
- port: {{ .Values.registry.service.port }}
targetPort: registry
protocol: TCP
name: registry
selector:
{{ include "docker-registry.matchLabels" . | indent 6 }}
{{- end -}}

View File

@@ -1,52 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "docker-registry-ui.fullname" . }}
labels:
{{ include "docker-registry-ui.labels" . | indent 4 }}
spec:
replicas: {{ .Values.ui.replicaCount }}
selector:
matchLabels:
{{ include "docker-registry-ui.matchLabels" . | indent 6 }}
template:
metadata:
labels:
{{ include "docker-registry-ui.matchLabels" . | indent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: registry-ui
image: "{{ .Values.ui.image.registry }}/{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}"
imagePullPolicy: {{ .Values.ui.image.pullPolicy }}
env:
- name: REGISTRY_TITLE
value: {{ .Values.ui.title| quote }}
- name: DELETE_IMAGES
value: {{ .Values.ui.delete_images| quote }}
- name: {{ include "docker-registry-ui.url-name" . }}
value: {{ include "docker-registry-ui.url-value" . | quote }}
- name: PULL_URL
value: {{ include "docker-registry-ui.pull" . | quote }}
ports:
- name: http
containerPort: 80
protocol: TCP
{{ include "docker-registry-ui.probes" . | indent 10 }}
resources:
{{- toYaml .Values.ui.resources | nindent 12 }}
{{- with .Values.ui.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ui.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ui.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -1,34 +0,0 @@
{{- if .Values.ui.ingress.enabled -}}
{{- $fullName := include "docker-registry-ui.fullname" . -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{ include "docker-registry-ui.labels" . | indent 4 }}
{{- with .Values.ui.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ui.ingress.tls }}
tls:
{{- range .Values.ui.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ui.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
- path: /
backend:
serviceName: {{ $fullName }}
servicePort: http
{{- end }}
{{- end }}

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "docker-registry-ui.fullname" . }}
labels:
{{ include "docker-registry-ui.labels" . | indent 4 }}
spec:
type: {{ .Values.ui.service.type }}
ports:
- port: {{ .Values.ui.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{ include "docker-registry-ui.matchLabels" . | indent 6 }}

View File

@@ -1,129 +0,0 @@
# Default values for docker-registry-ui.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
ui:
# title of the registry
title: "Docker registry UI"
# allow delete of images
delete_images: false
# UI behave as a proxy of the registry
proxy: true
replicaCount: 1
image:
registry: docker.io
repository: joxit/docker-registry-ui
tag: static
pullPolicy: Always
probe:
liveness: true
readiness: true
resources: {}
# If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
fullnameOverride: ""
service:
type: ClusterIP
port: 80
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: docker-registry-ui.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
registry:
external: false
# URL of the registry (requiered. Note: this wont work as localhost is inside the container. Only used if the registry is external)
url: http://localhost:5000
replicaCount: 1
# Image definition for the registry (Only used if the registry is not external)
image:
registry: docker.io
repository: registry
tag: 2.7.1
pullPolicy: Always
probe:
liveness: true
readiness: true
resources: {}
# If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
fullnameOverride: ""
persistence:
## If true, use a Persistent Volume Claim, If false, use emptyDir
##
enabled: false
## Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
## Persistent Volume Claim annotations
##
annotations:
## Persistent Volume Access Mode
##
accessModes:
# This have to be ReadWriteMany if replicaCount>1
- ReadWriteOnce
## Persistent Volume size
##
size: 1Gi
##
service:
type: ClusterIP
port: 5000
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: docker-registry.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
imagePullSecrets: []
nameOverride: ""

View File

@@ -10,7 +10,7 @@ kubectl apply -f *.yaml
Please note that you'll need a PV provisionner to be able to store the uploaded images.
## Dynamic installation
Edit the image tag in the ui-deployement.yaml file and set it to `latest`, then :
Edit the image tag in the ui-deployment.yaml file and set it to `latest`, then :
```sh
kubectl apply -f ui*.yaml

14
examples/pr-219/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Example for pull request #219
Basic usage for `DEFAULT_REGISTRIES` and `READ_ONLY_REGISTRIES`.
Behaviors:
- `DEFAULT_REGISTRIES`:
- will set the list of registries in the localstorage when the localstorage is empty.
- will overwrite the list of registries every time when `READ_ONLY_REGISTRIES=true`
- `READ_ONLY_REGISTRIES`:
- will remove dialog for Add and Remove registries
These options works only when `SINGLE_REGISTRY=false`
See [#219](https://github.com/Joxit/docker-registry-ui/pull/219)

View File

@@ -0,0 +1,39 @@
version: '2'
services:
registry_1:
image: registry:latest
restart: always
ports:
- 5000:5000
container_name: registry_1
environment:
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: "['*']"
REGISTRY_STORAGE_DELETE_ENABLED: 'true'
volumes:
- ./data:/var/lib/registry
registry_2:
image: registry:latest
restart: always
ports:
- 5001:5000
container_name: registry_2
environment:
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: "['*']"
REGISTRY_STORAGE_DELETE_ENABLED: 'true'
volumes:
- ./data:/var/lib/registry
ui:
image: joxit/docker-registry-ui:latest
restart: always
container_name: registry-ui
environment:
- REGISTRY_TITLE=Private Docker Registry
- DEFAULT_REGISTRIES=http://localhost:5000,http://localhost:5001
- DELETE_IMAGES=true
- READ_ONLY_REGISTRIES=true
- SINGLE_REGISTRY=false
ports:
- 80:80

View File

@@ -26,7 +26,9 @@ server {
#! if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
#! return 404;
#! }
#! proxy_http_version 1.1;
#! ${NGINX_PROXY_HEADERS}
#! ${NGINX_PROXY_PASS_HEADERS}
#! proxy_pass ${NGINX_PROXY_PASS_URL};
#! }

View File

@@ -1,8 +1,12 @@
{
"name": "docker-registry-ui",
"version": "2.0.7",
"version": "2.3.1",
"scripts": {
"start": "ROLLUP_SERVE=true rollup -c -w",
"format": "npm run format-html && npm run format-js && npm run format-riot",
"format-html": "find src rollup rollup.config.js -name '*.html' -exec prettier --config .prettierrc -w --parser html {} \\;",
"format-js": "find src rollup rollup.config.js -name '*.js' -exec prettier --config .prettierrc -w {} \\;",
"format-riot": "find src rollup rollup.config.js -name '*.riot' -exec prettier --config .prettierrc -w --parser html {} \\;",
"start": "rollup -c -w --environment ROLLUP_SERVE:true",
"build": "rollup -c",
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
},
@@ -14,28 +18,29 @@
"license": "AGPL-3.0",
"description": "A web UI for private docker registry",
"devDependencies": {
"@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",
"@babel/core": "^7.17.9",
"@babel/preset-env": "^7.16.0",
"@riotjs/compiler": "^6.1.3",
"@riotjs/observable": "^4.1.1",
"@riotjs/route": "^8.0.1",
"@rollup/plugin-babel": "^5.2.2",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-html": "^0.2.3",
"@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-html": "^0.2.4",
"@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-node-resolve": "^13.2.1",
"core-js": "^3.22.0",
"node-sass": "^7.0.1",
"prettier": "^2.6.2",
"riot": "^6.1.2",
"riot-mui": "github:joxit/riot-5-mui#4d68d7f",
"rollup": "^2.70.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-riot": "^6.0.0",
"rollup-plugin-scss": "^3.0.0",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-styles": "^3.14.1",
"rollup-plugin-styles": "^4.0.0",
"rollup-plugin-terser": "^7.0.2"
}
}

View File

@@ -12,6 +12,7 @@ import json from '@rollup/plugin-json';
import copy from 'rollup-plugin-copy';
import copyTransform from './rollup/copy-transform';
import license from './rollup/license';
import checkOutput from './rollup/check-output';
const useServe = process.env.ROLLUP_SERVE === 'true';
const output = useServe ? '.serve' : 'dist';
@@ -44,11 +45,12 @@ export default [
dir: output,
name: 'DockerRegistryUI',
format: 'iife',
sourcemap: useServe
sourcemap: useServe,
},
plugins: [emptyDirectories(output)].concat(
plugins,
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) })
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) }),
checkOutput(output)
),
},
];

40
rollup/check-output.js Normal file
View File

@@ -0,0 +1,40 @@
import fs from 'fs';
import path from 'path';
const files = [
'docker-registry-ui.css',
'docker-registry-ui.js',
'fonts/MaterialIcons-Regular.eot',
'fonts/MaterialIcons-Regular.svg',
'fonts/MaterialIcons-Regular.ttf',
'fonts/MaterialIcons-Regular.woff',
'fonts/MaterialIcons-Regular.woff2',
'fonts/Roboto-Bold.ttf',
'fonts/Roboto-Bold.woff',
'fonts/Roboto-Bold.woff2',
'fonts/Roboto-Light.ttf',
'fonts/Roboto-Light.woff',
'fonts/Roboto-Light.woff2',
'fonts/RobotoMono-Regular.eot',
'fonts/RobotoMono-Regular.ttf',
'fonts/RobotoMono-Regular.woff',
'fonts/RobotoMono-Regular.woff2',
'fonts/Roboto-Regular.eot',
'fonts/Roboto-Regular.ttf',
'fonts/Roboto-Regular.woff',
'fonts/Roboto-Regular.woff2',
'images/docker-logo.svg',
'index.html',
];
export default function (output) {
return {
name: 'check-output',
writeBundle: () => {
const missingFile = files.find((file) => !fs.existsSync(path.join(output, file)));
if (missingFile) {
throw new Error(`File ${missingFile} is missing after build`);
}
},
};
}

View File

@@ -15,4 +15,4 @@ export default `/*
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @license AGPL
*/`
*/`;

View File

@@ -16,11 +16,12 @@ 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)}">
<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 }" />
<material-waves center="true" color="#ddd"></material-waves>
<span>
<i class="material-icons">send</i>
{ state.image || state.repo }
@@ -28,17 +29,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{ state.nImages } images
<i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i>
</div>
<div if="{props.showCatalogNbTags && state.image}" class="item-count right">
{ state.nbTags } tags
<i class="material-icons animated"></i>
</div>
</span>
</material-card>
<catalog-element if="{ state.images }" filter-results="{ props.filterResults }"
<catalog-element
if="{ state.images }"
filter-results="{ props.filterResults }"
registry-url="{ props.registryUrl }"
on-notify="{ props.onnNotify }"
on-authentication="{ props.onAuthentication }"
show-catalog-nb-tags="{ props.showCatalogNbTags }"
class="animated {!state.expanded && !props.filterResults ? 'hide' : ''} {state.expanding ? 'expanding' : ''}"
each="{item in state.images}" item="{ item }" />
each="{item in state.images}"
item="{ item }"
></catalog-element>
</div>
<script>
import router from '../../scripts/router';
import {
matchSearch
} from '../search-bar.riot';
import { Http } from '../../scripts/http';
import { matchSearch } from '../search-bar.riot';
export default {
onBeforeMount(props, state) {
@@ -51,10 +63,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
state.repo = props.item.repo;
state.nImages = props.item.images.length;
}
if (props.showCatalogNbTags && state.image) {
this.getNbTags(props, state);
}
},
onBeforeUpdate(props, state) {
if (props.filterResults && state.images) {
state.nImages = state.images.filter(image => matchSearch(props.filterResults, image)).length;
state.nImages = state.images.filter((image) => matchSearch(props.filterResults, image)).length;
} else {
state.nImages = state.images && state.images.length;
}
@@ -66,20 +81,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
} else {
this.update({
expanded: !this.state.expanded,
expanding: true
expanding: true,
});
setTimeout(() => {
this.update({
expanding: false
expanding: false,
});
}, 50)
}, 50);
}
},
setLaunchListener(cb) {
this.triggerLaunch = cb;
getNbTags(props, state) {
const self = this;
const oReq = new Http({
onAuthentication: props.onAuthentication,
});
oReq.addEventListener('load', function () {
if (this.status === 200) {
const nbTags = (JSON.parse(this.responseText).tags || []).length;
self.update({ nbTags });
} else if (this.status === 404) {
props.onNotify('Server not found', true);
} else {
props.onNotify(this.responseText, true);
}
});
oReq.addEventListener('error', function () {
props.onNotify(this.getErrorMessage(), true);
});
oReq.open('GET', props.registryUrl + '/v2/' + state.image + '/tags/list');
oReq.send();
},
matchSearch
}
matchSearch,
};
</script>
<!-- End of tag -->
</catalog-element>
</catalog-element>

View File

@@ -26,50 +26,64 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<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 }"/>
<catalog-element
each="{ item in state.repositories }"
item="{ item }"
filter-results="{ props.filterResults }"
registry-url="{ props.registryUrl }"
on-notify="{ props.onNotify }"
on-authentication="{ props.onAuthentication }"
show-catalog-nb-tags="{ props.showCatalogNbTags }"
></catalog-element>
<script>
import CatalogElement from './catalog-element.riot'
import {
Http
} from '../../scripts/http';
import CatalogElement from './catalog-element.riot';
import { Http } from '../../scripts/http';
import { getRegistryServers } from '../../scripts/utils';
export default {
components: {
CatalogElement
CatalogElement,
},
state: {
registryName: '',
length: 0,
loadend: false,
repositories: []
repositories: [],
registryUrl: '',
},
onBeforeMount(props) {
this.state.registryName = props.registryName;
this.state.catalogElementsLimit = props.catalogElementsLimit;
},
onMounted(props) {
this.display(props, this.state)
onMounted(props, state) {
this.display(props, state);
},
onUpdated(props, state) {
this.display(props, state);
},
display(props, state) {
if (props.registryUrl === state.registryUrl) {
return;
}
state.registryUrl = props.registryUrl;
let repositories = [];
const self = this;
const oReq = new Http({
onAuthentication: this.props.onAuthentication
onAuthentication: this.props.onAuthentication,
});
oReq.addEventListener('load', function () {
if (this.status == 200) {
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) {
if (acc.length === 0 || acc[acc.length - 1].repo != repoName) {
acc.push({
repo: repoName,
images: []
images: [],
});
}
acc[acc.length - 1].images.push(e);
@@ -78,7 +92,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
acc.push(e);
return acc;
}, []);
} else if (this.status == 404) {
} else if (this.status === 404) {
self.props.onNotify('Server not found', true);
} else {
self.props.onNotify(this.responseText);
@@ -91,13 +105,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
self.update({
repositories,
nRepositories: repositories.length,
nImages: repositories.reduce((acc, e) => acc + (e.images && e.images.length || 1), 0),
loadend: true
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>
</catalog>

View File

@@ -31,9 +31,7 @@
</div>
</material-popup>
<script>
import {
getRegistryServers
} from '../../scripts/utils';
import { addRegistryServers } from '../../scripts/utils';
import router from '../../scripts/router';
export default {
@@ -51,14 +49,12 @@
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()
const url = addRegistryServers(input.value);
router.home();
this.props.onServerChange(url);
this.props.onClose()
this.props.onClose();
setTimeout(() => router.updateUrlQueryParam(url), 100);
}
}
},
};
</script>
</add-registry-url>
</add-registry-url>

View File

@@ -32,9 +32,7 @@
</div>
</material-popup>
<script>
import {
getRegistryServers
} from '../../scripts/utils';
import { addRegistryServers, getRegistryServers } from '../../scripts/utils';
import router from '../../scripts/router';
export default {
change(event) {
@@ -45,16 +43,14 @@
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()
const url = addRegistryServers(select.value);
router.home();
this.props.onServerChange(url);
this.props.onClose()
this.props.onClose();
setTimeout(() => router.updateUrlQueryParam(url), 100);
},
getRegistryServers
}
getRegistryServers,
};
</script>
<style>
:host select {
@@ -75,4 +71,4 @@
margin: 1.5em 0;
}
</style>
</change-registry-url>
</change-registry-url>

View File

@@ -32,66 +32,80 @@
</div>
</material-popup>
<script>
import {
Http
} from '../../scripts/http';
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);
const contentDigests = new Set();
toDelete.forEach((image) => {
if (image.contentDigest) {
contentDigests.add(image.contentDigest);
}
})
return tags.filter(image => digests.has(image.digest))
});
return tags.filter((image) => contentDigests.has(image.contentDigest));
},
deleteImages() {
this.props.toDelete.forEach(image => this.deleteImage(image, this.props));
this.props.toDelete.forEach((image) => this.getContentDigestThenDelete(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
});
getContentDigestThenDelete({ name, tag }, opts) {
const { registryUrl, onNotify, onAuthentication } = opts;
const oReq = new Http({ onAuthentication });
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
if (this.status === 200 || this.status === 202) {
oReq.getContentDigest(function (contentDigest) {
if (!contentDigest) {
onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
} else {
self.deleteImage({ name, tag, contentDigest }, opts);
}
});
} else if (this.status === 404) {
onNotify(`Manifest for ${name}:${tag} not found`, true);
} else {
onNotify(this.responseText);
}
});
oReq.open('GET', `${registryUrl}/v2/${name}/manifests/${tag}`);
oReq.setRequestHeader(
'Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json'
);
oReq.send();
},
deleteImage({ name, tag, contentDigest }, opts) {
const { registryUrl, ignoreError, onNotify, onAuthentication, onClick } = opts;
const oReq = new Http({ 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 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.open('DELETE', `${registryUrl}/v2/${name}/manifests/${contentDigest}`);
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
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 {
@@ -105,4 +119,4 @@
max-height: 250px;
}
</style>
</confirm-delete-image>
</confirm-delete-image>

View File

@@ -15,18 +15,35 @@
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>
<add-registry-url
if="{ !props.readOnlyRegistries }"
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
if="{ !props.readOnlyRegistries }"
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 }" />
<material-dropdown-list
items="{ dropdownItems.filter(item => item.ro || !props.readOnlyRegistries) }"
onSelect="{ onDropdownSelect }"
opened="{ state.isDropdownOpened }"
/>
</div>
<div class="overlay" onclick="{ onClick }" if="{ state.isDropdownOpened }"></div>
<script>
@@ -38,41 +55,48 @@
components: {
AddRegistryUrl,
ChangeRegistryUrl,
RemoveRegistryUrl
RemoveRegistryUrl,
},
dropdownItems: [{
title: 'Add URL',
name: 'add-registry-url'
}, {
title: 'Change URL',
name: 'change-registry-url'
}, {
title: 'Remove URL',
name: 'remove-registry-url'
}],
dropdownItems: [
{
title: 'Add URL',
name: 'add-registry-url',
ro: false,
},
{
title: 'Change URL',
name: 'change-registry-url',
ro: true,
},
{
title: 'Remove URL',
name: 'remove-registry-url',
ro: false,
},
],
onDropdownSelect(key, item) {
this.update({
[item.name]: true,
isDropdownOpened: false
isDropdownOpened: false,
});
},
onClose(name) {
return () => {
this.update({
[name]: false,
isDropdownOpened: false
})
}
isDropdownOpened: false,
});
};
},
onClick() {
this.update({
isDropdownOpened: !this.state.isDropdownOpened
})
}
}
isDropdownOpened: !this.state.isDropdownOpened,
});
},
};
</script>
<style>
:host > .container{
:host > .container {
position: absolute;
top: 0px;
right: 16px;
@@ -125,4 +149,4 @@
line-height: 36px;
}
</style>
</dialogs-menu>
</dialogs-menu>

View File

@@ -21,8 +21,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<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">
<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>
@@ -37,18 +42,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</material-popup>
<script>
import {
getRegistryServers
} from '../../scripts/utils';
import { getRegistryServers, removeRegistryServers } 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));
removeRegistryServers(url);
setTimeout(() => this.update(), 100);
},
getRegistryServers
}
getRegistryServers,
};
</script>
<style>
:host material-popup .popup material-button {
@@ -59,4 +61,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
color: #777;
}
</style>
</remove-registry-url>
</remove-registry-url>

View File

@@ -19,33 +19,65 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<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>
<dialogs-menu
if="{props.singleRegistry !== 'true'}"
on-notify="{ notifySnackbar }"
on-server-change="{ onServerChange }"
default-registries="{ props.defaultRegistries }"
read-only-registries="{ truthy(props.readOnlyRegistries) }"
></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 }" />
<catalog
registry-url="{ state.registryUrl }"
registry-name="{ state.name }"
catalog-elements-limit="{ state.catalogElementsLimit }"
on-notify="{ notifySnackbar }"
filter-results="{ state.filter }"
on-authentication="{ onAuthentication }"
show-catalog-nb-tags="{ truthy(props.showCatalogNbTags) }"
/>
</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>
<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 }"
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
></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>
<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 }"
history-custom-labels="{ stringToArray(props.historyCustomLabels) }"
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
></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>
<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>
@@ -56,29 +88,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<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>
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">License AGPL-3.0</a>
</li>
</ul>
</material-footer>
</footer>
<script>
import {
version
} from '../../package.json';
import {
Router,
Route,
} from '@riotjs/route'
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 SearchBar from './search-bar.riot';
import { stripHttps, getRegistryServers, setRegistryServers, truthy, stringToArray } from '../scripts/utils';
import router from '../scripts/router';
export default {
@@ -89,85 +112,89 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
DialogsMenu,
SearchBar,
Router,
Route
Route,
},
onUpdated(props, state) {
state.snackbarIsError = false;
state.snackbarMessage = undefined;
},
onBeforeMount(props) {
// props.singleRegistry === 'true' means old static version
const registryUrl = props.registryUrl ||
(props.singleRegistry === 'true' ? undefined : (router.getUrlQueryParam() || getRegistryServers(0))) ||
(window.location.origin + window.location.pathname.replace(/\/+$/, ''));
this.state.registryUrl = registryUrl.replace(/\/$/, '');
if (
(props.defaultRegistries && props.defaultRegistries.length > 0 && getRegistryServers().length === 0) ||
truthy(props.readOnlyRegistries)
) {
setRegistryServers(props.defaultRegistries);
}
// props.singleRegistry === 'true' means old static version
const registryUrl =
props.registryUrl ||
(props.singleRegistry === 'true' ? undefined : router.getUrlQueryParam() || getRegistryServers(0)) ||
window.location.origin + window.location.pathname.replace(/\/+$/, '');
this.state.registryUrl = registryUrl.replace(/\/$/, '').replace(/index(\.html?)?$/, '');
this.state.name = props.name || stripHttps(props.registryUrl);
this.state.catalogElementsLimit = props.catalogElementsLimit || 100000;
this.state.pullUrl = this.pullUrl(this.state.registryUrl, props.pullUrl);
this.state.useControlCacheHeader = props.useControlCacheHeader;
},
onServerChange(registryUrl) {
this.update({
registryUrl,
name: stripHttps(registryUrl),
pullUrl: this.pullUrl(registryUrl),
snackbarMessage: 'Registry server changed to `' + 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()
const { realm, service, scope } = opts;
const req = new XMLHttpRequest();
req.addEventListener('loadend', () => {
try {
const bearer = JSON.parse(req.responseText);
onAuthenticated(bearer)
onAuthenticated(bearer);
} catch (e) {
this.notifySnackbar(`Failed to log in: ${e.message}`, true)
this.notifySnackbar(`Failed to log in: ${e.message}`, true);
}
})
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
req.send()
});
req.open('GET', `${realm}?service=${service}&scope=${scope}`);
req.send();
} else {
onAuthenticated()
onAuthenticated();
}
},
onAuthenticationClose() {
this.update({
authenticationDialogOpened: false
})
authenticationDialogOpened: false,
});
},
pullUrl(registryUrl, pullUrl) {
const url = pullUrl ||
(registryUrl && registryUrl.length > 0 && registryUrl) ||
window.location.host;
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
snackbarIsError: isError || false,
});
} else if (message && message.message) {
this.update({
snackbarMessage: message.message,
snackbarIsError: message.isError
snackbarIsError: message.isError,
});
}
},
onSearch(value) {
this.update({
filter: value
})
filter: value,
});
},
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
router,
version,
truthy
}
truthy,
stringToArray,
};
</script>
</docker-registry-ui>
</docker-registry-ui>

View File

@@ -1,9 +1,7 @@
<search-bar>
<material-input placeholder="Search in page"></material-input>
<script>
import {
router
} from '@riotjs/route';
import { router } from '@riotjs/route';
export default {
onMounted(props, state) {
@@ -11,17 +9,29 @@
let value = '';
const notify = () => {
if (value !== input.value) {
props.onSearch(input.value.toLowerCase())
props.onSearch(input.value.toLowerCase());
}
value = input.value;
}
};
input.addEventListener('keyup', notify);
router.on.value(() => {
input.value = '';
notify();
})
}
}
});
window.addEventListener('keydown', (e) => {
// F3 or CTRL + F
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
// already focused, fallback to default behavior
if (document.activeElement === input) {
return true;
} else {
e.preventDefault();
input.focus();
}
}
});
},
};
export function matchSearch(search, value) {
return !search || (value && value.toLowerCase().indexOf(search) >= 0);
@@ -45,4 +55,4 @@
color: #fff;
}
</style>
</search-bar>
</search-bar>

View File

@@ -15,17 +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-element class="{ state.key }">
<div class="headline"><i class="material-icons">{ state.icon }</i>
<div class="headline">
<i class="material-icons">{ state.icon }</i>
<p>{ state.name }</p>
</div>
<div class="content">
<div class="value" if="{ state.value }"> { state.value }</div>
<div class="values value" each="{ value in state.values }" if="{ state.values }"> { value }</div>
<div class="value" if="{ state.value }">{ state.value }</div>
<div class="values value" each="{ value in state.values }" if="{ state.values }">{ value }</div>
</div>
<script>
import {
getHistoryIcon
} from '../../scripts/utils';
import { getHistoryIcon } from '../../scripts/utils';
export default {
onBeforeStart(props, state) {
state.key = props.entry.key;
@@ -48,14 +47,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return name;
} else if (name === 'os') {
return 'OS';
} else if (name.startsWith('custom-label-')) {
name = name.replace('custom-label-', '');
}
return name.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace('_', ' ')
return name
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/[_-]/g, ' ')
.split(' ')
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
.join(' ');
}
}
},
};
</script>
<style>
:host.Labels .value,
@@ -70,7 +72,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:host.docker_version .headline .material-icons {
background-size: 24px auto;
background-image: url("images/docker-logo.svg");
background-image: url('images/docker-logo.svg');
background-repeat: no-repeat;
}
@@ -103,4 +105,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
font-size: 12px;
}
</style>
</tag-history-element>
</tag-history-element>

View File

@@ -20,33 +20,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<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>
<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 />
<material-spinner></material-spinner>
</div>
<material-tabs if="{ state.archs && state.loadend }" useLine="{ true }" tabs="{ state.archs }"
onTabChanged="{ onTabChanged }" />
<material-tabs
if="{ state.archs && state.loadend }"
useLine="{ true }"
tabs="{ state.archs }"
onTabChanged="{ onTabChanged }"
></material-tabs>
<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 }" />
<tag-history-element
each="{ entry in element }"
if="{ entry.value && entry.value.length > 0}"
entry="{ entry }"
></tag-history-element>
</material-card>
<script>
import {
DockerImage
} from '../../scripts/docker-image';
import {
bytesToSize
} from '../../scripts/utils';
import { DockerImage } from '../../scripts/docker-image';
import { bytesToSize } from '../../scripts/utils';
import router from '../../scripts/router';
import TagHistoryElement from './tag-history-element.riot'
import TagHistoryElement from './tag-history-element.riot';
export default {
components: {
TagHistoryElement
TagHistoryElement,
},
onBeforeMount(props, state) {
state.elements = [];
@@ -54,9 +56,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
list: true,
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication
onAuthentication: props.onAuthentication,
useControlCacheHeader: props.useControlCacheHeader,
});
state.image.fillInfo()
state.image.fillInfo();
},
onMounted(props, state) {
state.image.on('blobs', this.processBlobs);
@@ -64,16 +67,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
},
onTabChanged(arch, idx) {
const state = this.state;
const {
registryUrl,
onNotify
} = this.props;
state.elements = []
state.image.variants[idx] = state.image.variants[idx] ||
const { registryUrl, onNotify, useControlCacheHeader } = this.props;
state.elements = [];
state.image.variants[idx] =
state.image.variants[idx] ||
new DockerImage(this.props.image, arch.digest, {
list: false,
registryUrl,
onNotify
onNotify,
useControlCacheHeader,
});
if (state.image.variants[idx].blobs) {
return this.processBlobs(state.image.variants[idx].blobs);
@@ -83,6 +85,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
},
processBlobs(blobs) {
const state = this.state;
const { historyCustomLabels } = this.props;
function exec(elt) {
const guiElements = [];
@@ -90,8 +93,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
const value = elt[attribute];
const guiElement = {
"key": attribute,
"value": modifySpecificAttributeTypes(attribute, value)
'key': attribute,
'value': modifySpecificAttributeTypes(attribute, value),
};
guiElements.push(guiElement);
}
@@ -99,32 +102,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return guiElements.sort(eltSort);
}
const elements = new Array(blobs.history.length + 1);
elements[0] = exec(getConfig(blobs));
elements[0] = exec(getConfig(blobs, { historyCustomLabels }));
blobs.history.forEach(function (elt, i) {
elements[blobs.history.length - i] = exec(elt)
elements[blobs.history.length - i] = exec(elt);
});
this.update({
elements,
loadend: true
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
}
title:
manifest.platform.os +
'/' +
manifest.platform.architecture +
(manifest.platform.variant ? manifest.platform.variant : ''),
digest: manifest.digest,
};
});
this.update({
archs
archs,
});
},
toTaglist() {
router.taglist(this.props.image);
}
}
},
};
const eltIdx = function (e) {
switch (e) {
case 'created':
@@ -158,7 +164,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return new Date(value).toLocaleString();
case 'created_by':
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
return (cmd && cmd[1]) || 'RUN'
return (cmd && cmd[1]) || 'RUN';
case 'size':
return bytesToSize(value);
case 'Entrypoint':
@@ -175,26 +181,48 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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;
}, {});
const getConfig = function (blobs, { historyCustomLabels }) {
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)) {
if (!res.author && res.Labels && res.Labels.maintainer) {
res.author = blobs.config.Labels.maintainer;
delete res.Labels.maintainer;
}
if (res.Labels) {
historyCustomLabels
.filter((label) => res.Labels[label])
.forEach((label) => {
res[`custom-label-${label}`] = res.Labels[label];
delete res.Labels[label];
});
}
return res;
};
</script>
</tag-history>
</tag-history>

View File

@@ -16,16 +16,19 @@
-->
<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.">
<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';
import { ERROR_CAN_NOT_READ_CONTENT_DIGEST } from '../../scripts/utils';
export default {
onMounted(props, state) {
this.load(props, state);
@@ -37,13 +40,13 @@
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}`
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.contentDigest}`;
}
},
load(props, state) {
if (props.target !== 'tag' && !props.image.digest) {
props.image.one('content-digest', (digest) => {
this.update()
if (props.target !== 'tag' && !props.image.contentDigest) {
props.image.one('content-digest', (contentDigest) => {
this.update();
});
props.image.trigger('get-content-digest');
}
@@ -60,8 +63,8 @@
document.execCommand('copy');
copyText.style.display = 'none';
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.')
}
}
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.');
},
};
</script>
</copy-to-clipboard>
</copy-to-clipboard>

View File

@@ -15,7 +15,7 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
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>
<div title="{ getTitle(props.image, state.chars) }">{ getContentDigest(props.image, state.chars) }</div>
<script>
export default {
onMounted(props, state) {
@@ -25,12 +25,12 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
this.load(props, state);
},
load(props, state) {
if (props.image.digest) {
if (props.image.contentDigest) {
return;
}
state.chars = -1;
props.image.one('content-digest', (digest) => {
this.digest = digest;
props.image.one('content-digest', (contentDigest) => {
this.contentDigest = contentDigest;
props.image.on('content-digest-chars', this.onResize);
props.image.trigger('get-content-digest-chars');
});
@@ -39,22 +39,22 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
onResize(chars) {
if (chars !== this.state.chars) {
this.update({
chars
chars,
});
}
},
getTitle(image, chars) {
return chars >= 70 ? '' : (image.digest || '');
return chars >= 70 ? '' : image.contentDigest || '';
},
getDigest(image, chars) {
getContentDigest(image, chars) {
if (chars >= 70) {
return image.digest || '';
return image.contentDigest || '';
} else if (chars <= 0) {
return '';
} else {
return image.digest && image.digest.slice(0, chars) + '...';
return image.contentDigest && image.contentDigest.slice(0, chars) + '...';
}
}
}
},
};
</script>
</image-content-digest>
</image-content-digest>

View File

@@ -15,27 +15,27 @@
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>
<div title="Creation date { getLocalDate(props.image) }">{ getDate(props.image) }</div>
<script>
import {
dateFormat,
} from '../../scripts/utils';
import { dateFormat } from '../../scripts/utils';
export default {
onMounted(props) {
props.image.one('creation-date', (date) => {
this.update({
date: date,
localDate: date.toLocaleString()
localDate: date && date.toLocaleString(),
});
});
props.image.trigger('get-date');
},
getDate(image) {
return dateFormat(image.creationDate)
return !image.ociImage ? `${dateFormat(image.creationDate)} ago` : 'Not Available';
},
getLocalDate(image) {
return (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
}
}
return !image.ociImage
? (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
: 'unavailable on OCI index/Buildkit export cache';
},
};
</script>
</image-date>
</image-date>

View File

@@ -17,9 +17,7 @@
<image-size>
<div title="Compressed size of your image.">{ getImageSize(props.image) }</div>
<script>
import {
bytesToSize,
} from '../../scripts/utils';
import { bytesToSize } from '../../scripts/utils';
export default {
onMounted(props, state) {
this.load(props, state);
@@ -33,15 +31,14 @@
}
props.image.on('size', (size) => {
this.update({
size
size,
});
});
props.image.trigger('get-size');
},
getImageSize(image) {
return bytesToSize(image.size)
}
}
return bytesToSize(image.size);
},
};
</script>
</image-size>
</image-size>

View File

@@ -21,11 +21,11 @@
onMounted(props) {
props.image.on('sha256', (sha256) => {
this.update({
sha256: sha256.substring(0, 19)
sha256: sha256 && sha256.substring(0, 19),
});
});
props.image.trigger('get-sha256');
}
}
},
};
</script>
</image-tag>
</image-tag>

View File

@@ -17,14 +17,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<pagination>
<div class="conatianer">
<div class="pagination-centered">
<material-button waves-color="rgba(158,158,158,.4)" each="{p in props.pages}"
<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)}">
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>
<script>
</script>
</pagination>
<script></script>
</pagination>

View File

@@ -15,29 +15,35 @@ 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 }">
<material-button
waves-center="true"
rounded="true"
waves-color="#ddd"
title="This will delete the image."
if="{ !props.multiDelete }"
disabled="{ !state.contentDigest }"
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
if="{ props.multiDelete }"
title="Select this tag to delete it."
disabled="{ !state.contentDigest }"
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';
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) => {
props.image.one('content-digest', (contentDigest) => {
this.update({
digest
contentDigest,
});
});
},
@@ -53,7 +59,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
handleCheckboxChange(checked) {
const action = checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
this.props.handleCheckboxChange(action, this.props.image);
}
}
},
};
</script>
</remove-image>
</remove-image>

View File

@@ -15,16 +15,34 @@ 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 title="This will show the history of given tag" waves-center="true" rounded="true" waves-color="#ddd"
onClick="{ routeToHistory }">
<material-button
title="{ buttonTittle() }"
waves-center="true"
rounded="true"
waves-color="#ddd"
onClick="{ routeToHistory }"
disabled="{ props.image.ociImage }"
>
<i class="material-icons">history</i>
</material-button>
<script>
import router from '../../scripts/router';
export default {
onMounted(props) {
props.image.one('oci-image', () => {
this.update();
});
},
buttonTittle() {
return !this.props.image.ociImage
? 'This will show the history of given tag'
: 'History is unavailable on OCI index/Buildkit export cache';
},
routeToHistory() {
router.history(this.props.image.name, this.props.image.tag)
}
}
if (!this.props.image.ociImage) {
router.history(this.props.image.name, this.props.image.tag);
}
},
};
</script>
</tag-history-button>
</tag-history-button>

View File

@@ -16,15 +16,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<tag-list>
<material-card class="header">
<div class="material-card-title-action ">
<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="source-hint">Sourced from { state.registryName + '/' + props.image }</div>
<div class="item-count">{ state.tags.length } tags</div>
</h2>
</div>
@@ -36,30 +34,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<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
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'
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,
@@ -71,11 +70,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
tags: [],
loadend: false,
asc: true,
page: router.getPageQueryParam() || 1
}
page: router.getPageQueryParam() || 1,
};
},
onMounted(props, state) {
this.display(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.
@@ -85,23 +84,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
state.tags = [];
const self = this;
const oReq = new Http({
onAuthentication: props.onAuthentication
onAuthentication: props.onAuthentication,
});
oReq.addEventListener('load', function () {
if (this.status == 200) {
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
}))
.map(
(tag) =>
new DockerImage(props.image, tag, {
list: true,
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication,
useControlCacheHeader: props.useControlCacheHeader,
})
)
.sort(compare);
window.requestAnimationFrame(self.onResize);
self.update({
page: Math.min(state.page, getNumPages(tags)),
tags
})
} else if (this.status == 404) {
tags,
});
} else if (this.status === 404) {
self.props.onNotify('Server not found', true);
} else {
self.props.onNotify(this.responseText, true);
@@ -113,7 +117,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
});
oReq.addEventListener('loadend', function () {
self.update({
loadend: true
loadend: true,
});
});
oReq.open('GET', props.registryUrl + '/v2/' + props.image + '/tags/list');
@@ -123,7 +127,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onPageUpdate(page) {
this.update({
page: page
page: page,
});
router.updatePageQueryParam(page);
},
@@ -143,8 +147,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// SHA256:12345678 + scaled between 1024 and 1440px
chars = 15 + 56 * ((innerWidth - 1024) / 416);
}
if (max > 20) chars -= (max - 20);
chars = Math.floor(chars)
if (max > 20) chars -= max - 20;
chars = Math.floor(chars);
this.state.tags.map(function (image) {
image.trigger('content-digest-chars', chars);
});
@@ -162,7 +166,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
},
getPageLabels,
getNumPages,
router
}
router,
};
</script>
</tag-list>
</tag-list>

View File

@@ -15,39 +15,61 @@ 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>
<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;">
<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') }">
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') }">
onclick="{() => onPageReorder('size') }"
>
Size
</th>
<th id="image-content-digest-header" if="{ props.showContentDigest }">Content Digest</th>
<th id="image-tag-header"
<th
id="image-tag-header"
class="{ props.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
onclick="{ onReverseOrder }">Tag
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 }"
<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 }">
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 }">
<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>
@@ -63,30 +85,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</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 }" />
<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 }" />
<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 }" />
<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 { getPage } from '../../scripts/utils';
import ImageDate from './image-date.riot';
import ImageSize from './image-size.riot';
import ImageTag from './image-tag.riot';
@@ -94,9 +128,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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 { 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';
@@ -118,18 +150,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
toDelete: new Set(),
multiDelete: false,
page: props.page,
}
};
},
onBeforeUpdate(props, state) {
if (state.page !== props.page) {
state.toDelete.clear();
}
state.page = props.page
state.page = props.page;
},
deleteImages() {
this.update({
confirmDeleteImage: true
})
confirmDeleteImage: true,
});
},
onConfirmDeleteImageClick() {
if (this.state.singleDeleteAction) {
@@ -137,22 +169,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
this.update({
singleDeleteAction: false,
confirmDeleteImage: false
})
confirmDeleteImage: false,
});
},
onRemoveImageHeaderChange(checked, event) {
if (event.altKey === true) {
const tags = getPage(this.props.tags, this.props.page);
tags.filter(image => matchSearch(this.props.filterResults, image.tag))
.forEach(tag => this.state.toDelete.add(tag));
tags
.filter((image) => matchSearch(this.props.filterResults, image.tag))
.forEach((tag) => this.state.toDelete.add(tag));
this.update({
multiDelete: true,
toDelete: this.state.toDelete
})
toDelete: this.state.toDelete,
});
} else {
this.update({
multiDelete: checked
})
multiDelete: checked,
});
}
},
onRemoveImageChange(action, image) {
@@ -177,8 +210,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
this.update({
toDelete: this.state.toDelete,
confirmDeleteImage,
singleDeleteAction
})
singleDeleteAction,
});
},
onReverseOrder() {
this.state.orderType = null;
@@ -188,30 +221,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onPageReorder(type) {
this.update({
orderType: type,
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc
})
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());
!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);
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
}
matchSearch,
};
export { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE };
</script>
</tag-table>
</tag-table>

View File

@@ -16,61 +16,80 @@
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- build:css docker-registry-ui.css -->
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/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" />
<!-- endbuild -->
<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>
<head>
<meta charset="UTF-8">
<!-- build:css docker-registry-ui.css -->
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/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">
<!-- endbuild -->
<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>
<!-- 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>
<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>
<script src="tags/tag-history.riot" type="riot/tag"></script>
<script src="tags/tag-history-element.riot" type="riot/tag"></script>
<script src="tags/taglist.riot" type="riot/tag"></script>
<script src="tags/image-tag.riot" type="riot/tag"></script>
<script src="tags/remove-image.riot" type="riot/tag"></script>
<script src="tags/copy-to-clipboard.riot" type="riot/tag"></script>
<script src="tags/dialogs/add.riot" type="riot/tag"></script>
<script src="tags/dialogs/change.riot" type="riot/tag"></script>
<script src="tags/dialogs/remove.riot" type="riot/tag"></script>
<script src="tags/dialogs/menu.riot" type="riot/tag"></script>
<script src="tags/image-size.riot" type="riot/tag"></script>
<script src="tags/image-date.riot" type="riot/tag"></script>
<script src="tags/image-content-digest.riot" type="riot/tag"></script>
<script src="tags/pagination.riot" type="riot/tag"></script>
<script src="tags/app.riot" type="riot/tag"></script>
<script src="scripts/http.js"></script>
<script src="scripts/script.js"></script>
<script src="scripts/utils.js"></script>
<!-- endbuild -->
</body>
</html>
<body>
<!-- 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}"
default-registries="${DEFAULT_REGISTRIES}"
read-only-registries="${READ_ONLY_REGISTRIES}"
show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"
history-custom-labels="${HISTORY_CUSTOM_LABELS}"
use-control-cache-header="${USE_CONTROL_CACHE_HEADER}"
>
</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"
show-catalog-nb-tags="true"
history-custom-labels="first_custom_labels,second_custom_labels"
use-control-cache-header="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>
<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>
<script src="tags/tag-history.riot" type="riot/tag"></script>
<script src="tags/tag-history-element.riot" type="riot/tag"></script>
<script src="tags/taglist.riot" type="riot/tag"></script>
<script src="tags/image-tag.riot" type="riot/tag"></script>
<script src="tags/remove-image.riot" type="riot/tag"></script>
<script src="tags/copy-to-clipboard.riot" type="riot/tag"></script>
<script src="tags/dialogs/add.riot" type="riot/tag"></script>
<script src="tags/dialogs/change.riot" type="riot/tag"></script>
<script src="tags/dialogs/remove.riot" type="riot/tag"></script>
<script src="tags/dialogs/menu.riot" type="riot/tag"></script>
<script src="tags/image-size.riot" type="riot/tag"></script>
<script src="tags/image-date.riot" type="riot/tag"></script>
<script src="tags/image-content-digest.riot" type="riot/tag"></script>
<script src="tags/pagination.riot" type="riot/tag"></script>
<script src="tags/app.riot" type="riot/tag"></script>
<script src="scripts/http.js"></script>
<script src="scripts/script.js"></script>
<script src="scripts/utils.js"></script>
<!-- endbuild -->
</body>
</html>

View File

@@ -46,7 +46,7 @@ export function compare(e1, e2) {
}
export class DockerImage {
constructor(name, tag, { list, registryUrl, onNotify, onAuthentication }) {
constructor(name, tag, { list, registryUrl, onNotify, onAuthentication, useControlCacheHeader }) {
this.name = name;
this.tag = tag;
this.chars = 0;
@@ -55,7 +55,9 @@ export class DockerImage {
registryUrl,
onNotify,
onAuthentication,
useControlCacheHeader,
};
this.ociImage = false;
observable(this);
this.on('get-size', function () {
if (this.size !== undefined) {
@@ -64,7 +66,7 @@ export class DockerImage {
return this.fillInfo();
});
this.on('get-sha256', function () {
if (this.size !== undefined) {
if (this.sha256 !== undefined) {
return this.trigger('sha256', this.sha256);
}
return this.fillInfo();
@@ -82,8 +84,8 @@ export class DockerImage {
return this.trigger('content-digest-chars', this.chars);
});
this.on('get-content-digest', function () {
if (this.digest !== undefined) {
return this.trigger('content-digest', this.digest);
if (this.contentDigest !== undefined) {
return this.trigger('content-digest', this.contentDigest);
}
return this.fillInfo();
});
@@ -96,7 +98,7 @@ export class DockerImage {
const oReq = new Http({ onAuthentication: this.opts.onAuthentication });
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
if (this.status === 200 || this.status === 202) {
const response = JSON.parse(this.responseText);
if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json' && self.opts.list) {
self.trigger('list', response);
@@ -107,22 +109,30 @@ export class DockerImage {
self.variants = [image];
return;
}
self.size = response.layers.reduce(function (acc, e) {
self.ociImage = response.mediaType === 'application/vnd.oci.image.index.v1+json';
self.layers = response.layers || response.manifests;
self.size = self.layers.reduce(function (acc, e) {
return acc + e.size;
}, 0);
self.sha256 = response.config.digest;
self.layers = response.layers;
self.sha256 = response.config && response.config.digest;
self.trigger('size', self.size);
self.trigger('sha256', self.sha256);
oReq.getContentDigest(function (digest) {
self.digest = digest;
self.trigger('content-digest', digest);
if (!digest) {
oReq.getContentDigest(function (contentDigest) {
self.contentDigest = contentDigest;
self.trigger('content-digest', contentDigest);
if (!contentDigest) {
self.opts.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
}
});
self.getBlobs(response.config.digest);
} else if (this.status == 404) {
if (!self.ociImage) {
self.getBlobs(self.sha256);
} else {
// Force updates
self.trigger('creation-date');
self.trigger('blobs');
self.trigger('oci-image');
}
} else if (this.status === 404) {
self.opts.onNotify(`Manifest for ${self.name}:${self.tag} not found`, true);
} else {
self.opts.onNotify(this.responseText);
@@ -131,16 +141,19 @@ export class DockerImage {
oReq.open('GET', `${this.opts.registryUrl}/v2/${self.name}/manifests/${self.tag}`);
oReq.setRequestHeader(
'Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' +
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json' +
(self.opts.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : '')
);
if (self.opts.useControlCacheHeader) {
oReq.setRequestHeader('Cache-Control', 'no-store, no-cache');
}
oReq.send();
}
getBlobs(blob) {
const oReq = new Http({ onAuthentication: this.opts.onAuthentication });
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
if (this.status === 200 || this.status === 202) {
const response = JSON.parse(this.responseText);
self.creationDate = new Date(response.created);
self.blobs = response;
@@ -155,8 +168,13 @@ export class DockerImage {
self.blobs.id = blob.replace('sha256:', '');
self.trigger('creation-date', self.creationDate);
self.trigger('blobs', self.blobs);
} else if (this.status == 404) {
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found`, true);
} else if (this.status === 404) {
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found: blob '${self.blobs}'`, true);
} else if (!this.responseText) {
self.opts.onNotify(
`Can"t get blobs for ${self.name}:${self.tag}: blob '${self.blobs}' (no message error)`,
true
);
} else {
self.opts.onNotify(this.responseText);
}

View File

@@ -52,8 +52,9 @@ export class Http {
switch (e) {
case 'loadend': {
self.oReq.addEventListener('loadend', function () {
if (this.status == 401 && !this.withCredentials) {
const tokenAuth = parseAuthenticateHeader(this.getResponseHeader('www-authenticate'));
if (this.status === 401 && !this.withCredentials) {
const tokenAuth =
this.hasHeader('www-authenticate') && parseAuthenticateHeader(this.getResponseHeader('www-authenticate'));
self.onAuthentication(tokenAuth, (bearer) => {
const req = new XMLHttpRequest();
req._url = self._url;
@@ -65,7 +66,7 @@ export class Http {
req.setRequestHeader(key, self._headers[key]);
}
if (bearer && bearer.token) {
req.setRequestHeader('Authorization', `Bearer ${bearer.token}`)
req.setRequestHeader('Authorization', `Bearer ${bearer.token}`);
} else {
req.withCredentials = true;
}

View File

@@ -1,8 +1,10 @@
const LOCAL_STORAGE_KEY = 'registryServer';
export function bytesToSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == undefined || isNaN(bytes)) {
return '?';
} else if (bytes == 0) {
} else if (bytes === 0) {
return '0 Byte';
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
@@ -74,7 +76,10 @@ export function getHistoryIcon(attribute) {
case 'ExposedPorts':
return 'router';
default:
'';
if (attribute.startsWith('custom-label-')) {
return 'label';
}
return '';
}
}
@@ -131,8 +136,13 @@ export function stripHttps(url) {
return url.replace(/^https?:\/\//, '');
}
function kebabToCamelCase(s) {
return s.replace(/-[a-z]/, (x) => x[1].toUpperCase());
}
export function eventTransfer(from, to) {
from.on('*', function (event, param) {
to[kebabToCamelCase(event)] = param;
to.trigger(event, param);
});
}
@@ -152,7 +162,7 @@ export const ERROR_CAN_NOT_READ_CONTENT_DIGEST = {
export function getRegistryServers(i) {
try {
const res = JSON.parse(localStorage.getItem('registryServer'));
const res = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (res instanceof Array) {
return !isNaN(i) ? res[i] : res.map((url) => url.trim().replace(/\/*$/, ''));
}
@@ -160,6 +170,28 @@ export function getRegistryServers(i) {
return !isNaN(i) ? '' : [];
}
export function setRegistryServers(registries) {
if (typeof registries === 'string') {
registries = registries.split(',');
} else if (!Array.isArray(registries)) {
throw new Error('setRegistries must be called with string or array parameter');
}
registries = registries.map((registry) => registry.replace(/\/*$/, ''));
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(registries));
}
export function addRegistryServers(registry) {
const url = registry.trim().replace(/\/*$/, '');
const registryServer = getRegistryServers().filter((e) => e !== url);
setRegistryServers([url].concat(registryServer));
return url;
}
export function removeRegistryServers(registry) {
const registryServers = getRegistryServers().filter((e) => e !== registry);
setRegistryServers(registryServers);
}
export function encodeURI(url) {
if (!url) {
return;
@@ -175,5 +207,9 @@ export function decodeURI(url) {
}
export function truthy(value) {
return value === true || value === "true";
}
return value === true || value === 'true';
}
export function stringToArray(value) {
return value && typeof value === 'string' ? value.split(',') : [];
}