mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2026-02-19 21:29:51 +00:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccd349b7d5 | ||
|
|
d87cd44a00 | ||
|
|
b8802ef7ab | ||
|
|
e6c20afcf3 | ||
|
|
1220825f31 | ||
|
|
501d0d72a0 | ||
|
|
be813e6617 | ||
|
|
b4e6369a71 | ||
|
|
c9ede6fe61 | ||
|
|
656914f0d7 | ||
|
|
f8c5010fd1 | ||
|
|
42f19fcef7 | ||
|
|
f9620639bf | ||
|
|
d1700ccf74 | ||
|
|
991eaf932d | ||
|
|
e2ee319d4a | ||
|
|
06d6293e79 | ||
|
|
00fe443a7c | ||
|
|
6e7fc1508e | ||
|
|
178cd5a59d | ||
|
|
da9591609e | ||
|
|
f0a40d6087 | ||
|
|
01d8bcfccd | ||
|
|
c60c2f3e95 | ||
|
|
241ee0fd13 | ||
|
|
2e915a82b1 | ||
|
|
e3d592ac65 | ||
|
|
531f9400a0 | ||
|
|
d7f6cd7e1a | ||
|
|
8524c0d18b | ||
|
|
8b7bd6cfb8 | ||
|
|
f56cd055a2 | ||
|
|
79203787bd | ||
|
|
32d895a09d | ||
|
|
bc26eca908 | ||
|
|
e301dda408 | ||
|
|
ce93a4da92 | ||
|
|
7296c2e727 | ||
|
|
94fcd53792 | ||
|
|
95558fe75a | ||
|
|
44f3242b85 | ||
|
|
9b05503e77 | ||
|
|
980c5ddaa9 | ||
|
|
2eeae54bca | ||
|
|
f7fe27c07e | ||
|
|
48aba129ce | ||
|
|
a2e81ac12c | ||
|
|
a48ce7ef75 | ||
|
|
04f4c204d8 | ||
|
|
41d71a1991 | ||
|
|
b3fbaff578 | ||
|
|
5e2ead0c2a | ||
|
|
70d5ae2601 | ||
|
|
3b0e63ff12 | ||
|
|
b6144ae13f | ||
|
|
65bbac3453 | ||
|
|
d380a767af | ||
|
|
36c44b19a3 | ||
|
|
28310f9804 | ||
|
|
6296a192d6 | ||
|
|
a0c88d06f0 | ||
|
|
eec736d4e8 | ||
|
|
9e99b08b82 | ||
|
|
0f805daafa | ||
|
|
8bd1f31c9c | ||
|
|
7777ff28df | ||
|
|
4fee7b44d3 | ||
|
|
79960ea52d | ||
|
|
7716f8b44a | ||
|
|
0ac7a151d9 | ||
|
|
ef149bf1cc | ||
|
|
e5a406a6ba | ||
|
|
dbb746981a | ||
|
|
92fc37adb4 | ||
|
|
02210e0943 | ||
|
|
0199f87087 | ||
|
|
3399030e4e | ||
|
|
660a938d6e | ||
|
|
7356591292 |
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Hi, I use this docker registry UI and I have an issue...
|
||||
|
||||
## Bug description
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
## How to Reproduce
|
||||
|
||||
For UI bug, steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
For service bug, steps to reproduce the behavior:
|
||||
<!-- Remove your credentials or anonymize them -->
|
||||
My docker-compose file
|
||||
```yml
|
||||
|
||||
```
|
||||
|
||||
My private docker registry configuration
|
||||
```yml
|
||||
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## System information
|
||||
|
||||
- OS: [e.g. Debian 10, Windows, Android 9...]
|
||||
<!-- Browser is only for UI bugs -->
|
||||
- Browser:
|
||||
- Name: [e.g. Chrome, Firefox...]
|
||||
- Version: [e.g. 22]
|
||||
- Docker registry UI:
|
||||
- Version: [e.g. 1.4.0]
|
||||
- Server: [docker or dist]
|
||||
<!-- Only for Docker and for where the UI is hosted -->
|
||||
- Docker version: [e.g. 19.03]
|
||||
- Docker registry ui tag: [latest, static, master, 1.4-static...]
|
||||
- OS/Arch: [e.g. linux/amd64]
|
||||
- Tools: [e.g. docker-compose, kubernets..]
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
10
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ node_modules
|
||||
package-lock.json
|
||||
registry-data
|
||||
.idea
|
||||
_site
|
||||
_site
|
||||
*.orig
|
||||
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at contact@joxit.dev. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
38
CONTRIBUTORS.md
Normal file
38
CONTRIBUTORS.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Contributors
|
||||
|
||||
## Committers
|
||||
|
||||
- Jones Magloire [@Joxit](https://github.com/Joxit)
|
||||
- Lennart Blom [@lennartblom](https://github.com/lennartblom)
|
||||
- Jakob Ackermann [@das7pad](https://github.com/das7pad)
|
||||
- Olivier Nizet [@onizet](https://github.com/onizet)
|
||||
- Anton Antonov [@syndbg](https://github.com/syndbg)
|
||||
- Bertrand Pechenot [@berpec](https://github.com/berpec)
|
||||
- Sébastien Huss [@sebt3](https://github.com/sebt3)
|
||||
- Vladimir Kozyrev [@fieryvova](https://github.com/fieryvova)
|
||||
- Haibo Jia [@bluethon](https://github.com/bluethon)
|
||||
- Manuel Leitold [@agrippa1994](https://github.com/agrippa1994)
|
||||
- Murad [@muradheydarov](https://github.com/muradheydarov)
|
||||
- Giacomo Mazzamuto [@gmazzamuto](https://github.com/gmazzamuto)
|
||||
- Joe Bureau [@jabstone](https://github.com/jabstone)
|
||||
|
||||
## Because committers are not the only contributors
|
||||
|
||||
- Ewout Prangsma [@ewoutp](https://github.com/ewoutp)
|
||||
- [@soosap](https://github.com/soosap)
|
||||
- Madhukar Mishra [@madhukar93](https://github.com/madhukar93)
|
||||
- Yoan Blanc [@greut](https://github.com/greut)
|
||||
- [@samuelmaier](https://github.com/samuelmaier)
|
||||
- [@sbaloo](https://github.com/sbaloo)
|
||||
- [@rucciva](https://github.com/rucciva)
|
||||
- [@wuyue92tree](https://github.com/wuyue92tree)
|
||||
- Giovanni Toraldo [@gionn](https://github.com/gionn)
|
||||
- [@marcusblake](https://github.com/marcusblake)
|
||||
- Dario [@pidario](https://github.com/pidario)
|
||||
- Jernej K. [@Cvetk0](https://github.com/Cvetk0)
|
||||
- Cristian Posoiu [@cr1st1p](https://github.com/cr1st1p)
|
||||
- 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)
|
||||
- Dario Piombo [@pidario](https://github.com/pidario)
|
||||
54
README.md
54
README.md
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Project Page
|
||||
title: Docker Registry User Interface
|
||||
---
|
||||
|
||||
# Docker Registry UI
|
||||
@@ -20,7 +20,7 @@ In the **static interface**, it will connect to a single registry and will not c
|
||||
|
||||
This web user interface uses [Riot](https://github.com/Riot/riot) the react-like user interface micro-library and [riot-mui](https://github.com/kysonic/riot-mui) components.
|
||||
|
||||
## [Project Page](https://joxit.dev/docker-registry-ui) and [Live Demo](https://joxit.dev/docker-registry-ui/demo/)
|
||||
## [Project Page](https://joxit.dev/docker-registry-ui), [Live Demo](https://joxit.dev/docker-registry-ui/demo/), [Examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples)
|
||||
|
||||

|
||||
|
||||
@@ -45,6 +45,35 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- Use `joxit/docker-registry-ui:static` as reverse proxy (with `REGISTRY_URL` environment variable) to your docker registry (This will avoid CORS) **static interface**.
|
||||
- Add Title when using `REGISTRY_URL` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)) **static interface**.
|
||||
- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)) **static interface**.
|
||||
- Add custom header via environment variable and file via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) **static interface**
|
||||
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)).
|
||||
- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)).
|
||||
- Multi arch support in history page (see [#130](https://github.com/Joxit/docker-registry-ui/issues/130) and [#134](https://github.com/Joxit/docker-registry-ui/pull/134))
|
||||
|
||||
## FAQ
|
||||
|
||||
- Why, when I delete all tags of an image, the image is still in the UI ?
|
||||
- This is a limitation of docker registry, the garbage collector don't remove empty images. If you want to delete dangling images, you will need to delete the folder in your registry data. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77))
|
||||
- Why the image size in the UI is not the same as displayed during `docker images` ?
|
||||
- The UI displays the compressed size of the image and not the extracted size version.
|
||||
- Can I use HTTPS on the UI ?
|
||||
- Yes, put your favourite reverse proxy on the front of the UI. Your reverse proxy will take care of HTTPS connection.
|
||||
- Does the UI support authentication ?
|
||||
- Yes, but it supports only basic auth. It's a simple standalone frontend, it will use your browser window for authentication.
|
||||
- Can I use the UI and docker client with an insecure registry (registry url without https) ?
|
||||
- Yes you can, you must first configure your docker client. (see [#76](https://github.com/Joxit/docker-registry-ui/issues/76))
|
||||
- What does Mixed Content error mean ?
|
||||
- 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)).
|
||||
- Can I use the docker registry ui as a standalone application (with Electron) ?
|
||||
- Yes, check out the example [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron). (see [#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- 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))
|
||||
|
||||
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples) or open an issue.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -119,11 +148,12 @@ docker run -d -p 80:80 joxit/docker-registry-ui
|
||||
|
||||
Some env options are available for use this interface for only one server.
|
||||
|
||||
- `URL`: set the static URL to use (You will need CORS configuration). Example: `http://127.0.0.1:5000`. (`Required`)
|
||||
- `REGISTRY_URL`: your docker registry URL to contact (CORS configuration is not needed). Example: `http://my-docker-container:5000`. (Can't be used with `URL`, since 0.3.2).
|
||||
- [`URL`](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone): set the static URL to use (You will need CORS configuration). Example: `http://127.0.0.1:5000`. (`Required`)
|
||||
- [`REGISTRY_URL`](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy): your docker registry URL to contact (CORS configuration is not needed). Example: `http://my-docker-container:5000`. (Can't be used with `URL`, since 0.3.2).
|
||||
- `DELETE_IMAGES`: if this variable is empty or `false`, delete feature is deactivated. It is activated otherwise.
|
||||
- `REGISTRY_TITLE`: Set a custom title for your user interface when using `REGISTRY_URL` (since 0.3.4).
|
||||
- `PULL_URL`: Set a custom url for the docker pull command, this is useful when you use `REGISTRY_URL` and your registry is on a different host (since 1.1.0).
|
||||
- [`NGINX_PROXY_HEADER_*`](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers): Set custom headers for your docker registry, usefull when you want to add your credentials. (Can be use only with `REGISTRY_URL`).
|
||||
|
||||
Example with `URL` option.
|
||||
|
||||
@@ -157,10 +187,13 @@ If your docker registry need credentials, you will need to send these HEADERS:
|
||||
http:
|
||||
headers:
|
||||
Access-Control-Allow-Origin: ['<your docker-registry-ui url>']
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS'] # Optional
|
||||
```
|
||||
|
||||
An alternative for CORS issues is a plugin on your browser, more info [here](https://github.com/Joxit/docker-registry-ui/issues/25#issuecomment-621104846) (thank you [xmontero](https://github.com/xmontero)).
|
||||
|
||||
## Using delete
|
||||
|
||||
For deleting images, you need to activate the delete feature in your registry:
|
||||
@@ -202,7 +235,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://127.0.0.1:8001']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
@@ -212,12 +245,19 @@ auth:
|
||||
path: /etc/docker/registry/htpasswd
|
||||
```
|
||||
|
||||
## Standalone Application
|
||||
If you do not want to install the docker-registry-ui on your server, you may
|
||||
check out the [Electron](examples/electron/README.md) standalone application.
|
||||
|
||||
## All examples
|
||||
|
||||
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
|
||||
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone)
|
||||
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/master/examples/traefik)
|
||||
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/88))
|
||||
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
|
||||
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
|
||||
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
|
||||
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
|
||||
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
|
||||
@@ -2,10 +2,12 @@ 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
|
||||
theme: jekyll-theme-cayman
|
||||
remote_theme: joxit/joxit.github.io
|
||||
author: Jones Magloire
|
||||
twitter:
|
||||
username: Joxit
|
||||
instagram:
|
||||
username: jox.it
|
||||
defaults:
|
||||
- scope:
|
||||
path: ""
|
||||
|
||||
@@ -18,9 +18,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
|
||||
COPY bin/entrypoint /bin
|
||||
|
||||
ENTRYPOINT entrypoint
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
|
||||
@@ -18,9 +18,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
|
||||
COPY bin/entrypoint /bin
|
||||
|
||||
ENTRYPOINT entrypoint
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
$@
|
||||
|
||||
sed -i "s,\${URL},${URL}," scripts/docker-registry-ui.js
|
||||
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," scripts/docker-registry-ui.js
|
||||
sed -i "s,\${PULL_URL},${PULL_URL}," scripts/docker-registry-ui.js
|
||||
@@ -8,13 +8,33 @@ if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
|
||||
sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
if [ -n "${REGISTRY_URL}" ] ; then
|
||||
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," /etc/nginx/conf.d/default.conf
|
||||
sed -i "s,#!,," /etc/nginx/conf.d/default.conf
|
||||
if [ "${SHOW_CONTENT_DIGEST}" = false ] ; then
|
||||
sed -i -r "s/(showContentDigest[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
if [ -z "$@" ]; then
|
||||
nginx -g "daemon off;"
|
||||
else
|
||||
$@
|
||||
if [ -n "${CATALOG_ELEMENTS_LIMIT}" ] ; then
|
||||
sed -i -r "s/(catalogElementsLimit[:=])[^,;]*/\1${CATALOG_ELEMENTS_LIMIT}/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
get_nginx_proxy_headers() {
|
||||
(
|
||||
env &&
|
||||
if [ -f "/etc/nginx/.env" ]; then
|
||||
cat /etc/nginx/.env
|
||||
# Force new line
|
||||
echo ""
|
||||
fi
|
||||
) | while read e; do
|
||||
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_HEADER_')" ]; then
|
||||
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_HEADER_//' | sed 's/_/-/g')
|
||||
value=${e#*=}
|
||||
echo -n "proxy_set_header ${key} \"${value}\"; "
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if [ -n "${REGISTRY_URL}" ] ; then
|
||||
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," /etc/nginx/conf.d/default.conf
|
||||
sed -i "s^\${NGINX_PROXY_HEADERS}^$(get_nginx_proxy_headers)^" /etc/nginx/conf.d/default.conf
|
||||
sed -i "s,#!,," /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
|
||||
13
bin/fill-registry
Executable file
13
bin/fill-registry
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
for i in alpine chronograf:alpine consul debian jawg/mapnik3 nginx:alpine postgres:alpine redis:alpine telegraf:alpine joxit/docker-registry-ui joxit/kosmtik joxit/node joxit/rust-openssl joxit/tile-server-ui; do
|
||||
docker pull $i
|
||||
docker tag $i 127.0.0.1:5000/$i
|
||||
docker push 127.0.0.1:5000/$i
|
||||
done
|
||||
|
||||
for i in arm32v7-static 1.2-debian-static master-static 1.2 arm64v8 arm32v7 arm64v8-static master 1.2-debian latest static debian-static debian 1.2-static 1.1 1.1-static 1.1-debian-static 1.1-debian ; do
|
||||
docker pull joxit/docker-registry-ui:$i
|
||||
docker tag joxit/docker-registry-ui:$i 127.0.0.1:5000/joxit/docker-registry-ui:$i
|
||||
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$i
|
||||
done
|
||||
@@ -18,9 +18,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
|
||||
COPY bin/entrypoint /bin
|
||||
|
||||
ENTRYPOINT entrypoint
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../dist/vendor.css">
|
||||
<link rel="stylesheet" href="../dist/style.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en" rel="stylesheet" type="text/css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="description" content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
|
||||
|
||||
BIN
dist/fonts/Roboto-Bold.ttf
vendored
Normal file
BIN
dist/fonts/Roboto-Bold.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Bold.woff
vendored
Normal file
BIN
dist/fonts/Roboto-Bold.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Bold.woff2
vendored
Normal file
BIN
dist/fonts/Roboto-Bold.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Light.ttf
vendored
Normal file
BIN
dist/fonts/Roboto-Light.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Light.woff
vendored
Normal file
BIN
dist/fonts/Roboto-Light.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Light.woff2
vendored
Normal file
BIN
dist/fonts/Roboto-Light.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Regular.eot
vendored
Normal file
BIN
dist/fonts/Roboto-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Regular.ttf
vendored
Normal file
BIN
dist/fonts/Roboto-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Regular.woff
vendored
Normal file
BIN
dist/fonts/Roboto-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/Roboto-Regular.woff2
vendored
Normal file
BIN
dist/fonts/Roboto-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/RobotoMono-Regular.eot
vendored
Normal file
BIN
dist/fonts/RobotoMono-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/RobotoMono-Regular.ttf
vendored
Normal file
BIN
dist/fonts/RobotoMono-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/RobotoMono-Regular.woff
vendored
Normal file
BIN
dist/fonts/RobotoMono-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/RobotoMono-Regular.woff2
vendored
Normal file
BIN
dist/fonts/RobotoMono-Regular.woff2
vendored
Normal file
Binary file not shown.
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -13,4 +13,4 @@
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="vendor.css"><link rel="stylesheet" href="style.css"><link href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en" 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><app></app><script src="scripts/vendor.js"></script><script src="scripts/docker-registry-ui.js"></script></body></html>
|
||||
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="vendor.css"><link rel="stylesheet" href="style.css"><meta name="viewport" content="width=device-width,initial-scale=1"><meta property="og:site_name" content="Docker Registry UI"><meta name="twitter:card" content="summary"><meta name="twitter:site" content="@Joxit"><meta name="twitter:creator" content="@Jones Magloire"><title>Docker Registry UI</title></head><body><app></app><script src="scripts/vendor.js"></script><script src="scripts/docker-registry-ui.js"></script></body></html>
|
||||
2
dist/scripts/docker-registry-ui-static.js
vendored
2
dist/scripts/docker-registry-ui-static.js
vendored
File diff suppressed because one or more lines are too long
2
dist/scripts/docker-registry-ui.js
vendored
2
dist/scripts/docker-registry-ui.js
vendored
File diff suppressed because one or more lines are too long
2
dist/style.css
vendored
2
dist/style.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 764 KiB After Width: | Height: | Size: 686 KiB |
11
examples/README.md
Normal file
11
examples/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## Examples
|
||||
|
||||
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
|
||||
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone)
|
||||
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/master/examples/traefik)
|
||||
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
|
||||
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
|
||||
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
|
||||
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
|
||||
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
8
examples/electron/.gitignore
vendored
Normal file
8
examples/electron/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# NPM renames .gitignore to .npmignore
|
||||
# In order to prevent that, we remove the initial "."
|
||||
# And the CLI then renames it
|
||||
|
||||
dist/
|
||||
node_modules/
|
||||
Registry*
|
||||
.cache
|
||||
57
examples/electron/README.md
Normal file
57
examples/electron/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Standalone Application
|
||||
|
||||
## Overview
|
||||
|
||||
This standalone application is based on Electron which encapsulates the whole
|
||||
docker-registry-ui in a single executable, that can be run on your local
|
||||
computer.
|
||||
|
||||
## Building
|
||||
* Check out or download the repository, open a terminal at the checkout
|
||||
directory, download the dependencies and build the web app:
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
* After building the web application, navigate to the ```electron``` directory
|
||||
and execute following commands to build the executable:
|
||||
```bash
|
||||
cd electron
|
||||
npm install
|
||||
npm run dist
|
||||
```
|
||||
If you encounter any issues, please check the troubleshooting below.
|
||||
|
||||
|
||||
## Password Protected Registries
|
||||
If you want to interact with password protected Docker Registries, this
|
||||
application will use the keystore of your system to gather the credentials for
|
||||
accessing the Registry.
|
||||
|
||||
This is accomplished with the [keytar](https://www.npmjs.com/package/keytar)
|
||||
package. In concjunction with keytar, the integrated credential
|
||||
helper supports you with managing the credentials to the Registries.
|
||||
|
||||

|
||||
|
||||
|
||||
## Troubleshooting
|
||||
* Problem: The application does not start with ```npm start``` and exits with following message:
|
||||
```
|
||||
[7742:0509/001117.199224:FATAL:setuid_sandbox_host.cc(157)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that ./node_modules/electron dist/chrome-sandbox is owned by root and has mode 4755.
|
||||
```
|
||||
|
||||
Solution: Add proper rights to the chrome-sanbox
|
||||
```bash
|
||||
sudo chown root ./node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
|
||||
```
|
||||
|
||||
* Problem: I am on Linux and to not have any password wallet for keytar.
|
||||
|
||||
Solution: Install following dependencies according to the official [setup instructions](https://atom.github.io/node-keytar/) for keytar on Linux:
|
||||
* Debian/Ubuntu: ```sudo apt-get install libsecret-1-dev```
|
||||
* Red Hat-based: ```sudo yum install libsecret-devel```
|
||||
* Arch Linux: ```sudo pacman -S libsecret```
|
||||
|
||||
|
||||
8
examples/electron/authentication/index.html
Normal file
8
examples/electron/authentication/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
211
examples/electron/authentication/index.tsx
Normal file
211
examples/electron/authentication/index.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import * as React from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {render} from "react-dom";
|
||||
import * as keytar from 'keytar';
|
||||
import {ipcRenderer} from 'electron';
|
||||
import {
|
||||
Button,
|
||||
createMuiTheme,
|
||||
CssBaseline,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
ThemeProvider,
|
||||
useMediaQuery
|
||||
} from "@material-ui/core";
|
||||
import {Alert, AlertTitle} from '@material-ui/lab';
|
||||
import {blue} from "@material-ui/core/colors";
|
||||
import {Add as AddIcon, Delete as DeleteIcon, Save as SaveIcon} from "@material-ui/icons";
|
||||
|
||||
const mainStyle = makeStyles((theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
main: {
|
||||
flexGrow: 1,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
function CredentialRow({credential, index, onDelete, onUpdate}) {
|
||||
const [account, setAccount] = useState(credential?.account || '');
|
||||
const [password, setPassword] = useState(credential?.password || '');
|
||||
|
||||
const style = mainStyle();
|
||||
return (<TableRow>
|
||||
<TableCell>
|
||||
<TextField
|
||||
className={style.input}
|
||||
type="text"
|
||||
placeholder='https://user@someregistry:5000/'
|
||||
value={account} variant="outlined"
|
||||
onChange={(e) => {
|
||||
setAccount(e.target.value)
|
||||
}}/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField type="password"
|
||||
className={style.input}
|
||||
variant="outlined"
|
||||
placeholder='Password'
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value)
|
||||
}}/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={async () => await onUpdate(credential, index, {account, password})}>
|
||||
<SaveIcon/>
|
||||
</IconButton>
|
||||
<IconButton onClick={async () => await onDelete(credential, index,)}>
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>);
|
||||
}
|
||||
|
||||
|
||||
function CredentialsTable({onError}) {
|
||||
const [credentials, setCredentials] = useState(null);
|
||||
|
||||
async function loadItems() {
|
||||
try {
|
||||
const credentials = await keytar.findCredentials('docker-registry-ui');
|
||||
for (const credential of credentials) {
|
||||
// fix for windows
|
||||
credential.password = credential.password.replace(/\000+/g, '');
|
||||
}
|
||||
setCredentials(credentials);
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item, index) {
|
||||
// delete an item that has not been stored yet
|
||||
if (!item) {
|
||||
const newCredentials = [...credentials];
|
||||
newCredentials.splice(index, 1);
|
||||
setCredentials(newCredentials);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await keytar.deletePassword('docker-registry-ui', item.account);
|
||||
await loadItems();
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate(oldCredentials, index, newCredentials) {
|
||||
try {
|
||||
await handleDelete(oldCredentials, index);
|
||||
await keytar.setPassword('docker-registry-ui', newCredentials.account, newCredentials.password);
|
||||
await loadItems();
|
||||
} catch (e) {
|
||||
console.error("Error while updating key: ", e);
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
await loadItems();
|
||||
};
|
||||
|
||||
load();
|
||||
return;
|
||||
}, []);
|
||||
|
||||
if (credentials === null) {
|
||||
return <LinearProgress/>
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Host of the registry including username</TableCell>
|
||||
<TableCell>Password</TableCell>
|
||||
<TableCell align='right'>
|
||||
<IconButton onClick={() => {
|
||||
setCredentials([...credentials, null])
|
||||
}} disabled={credentials.includes(null)}>
|
||||
<AddIcon/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{credentials.map((credential, index) => <CredentialRow
|
||||
onDelete={handleDelete}
|
||||
onUpdate={handleUpdate}
|
||||
index={index}
|
||||
key={credential?.account || ''}
|
||||
credential={credential}/>)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
const [error, setError] = useState();
|
||||
const classes = mainStyle();
|
||||
|
||||
const theme = React.useMemo(
|
||||
() =>
|
||||
createMuiTheme({
|
||||
palette: {
|
||||
type: prefersDarkMode ? 'dark' : 'light',
|
||||
primary: blue,
|
||||
},
|
||||
}),
|
||||
[prefersDarkMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline/>
|
||||
<div className={classes.root}>
|
||||
{error && <Alert severity='error' onClose={() => {
|
||||
setError(null)
|
||||
}}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
{error}
|
||||
</Alert>}
|
||||
<main className={classes.main}>
|
||||
<CredentialsTable onError={setError}/>
|
||||
</main>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(<App/>, document.getElementById("root"));
|
||||
|
||||
// @ts-ignore
|
||||
if (module.hot) {
|
||||
// @ts-ignore
|
||||
module.hot.accept();
|
||||
}
|
||||
|
||||
BIN
examples/electron/doc/assets/authentication.gif
Normal file
BIN
examples/electron/doc/assets/authentication.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
229
examples/electron/index.js
Normal file
229
examples/electron/index.js
Normal file
@@ -0,0 +1,229 @@
|
||||
const {app, BrowserWindow, globalShortcut, Menu} = require('electron');
|
||||
const isDevMode = require('electron-is-dev');
|
||||
const keytar = require('keytar');
|
||||
const url = require('url');
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
// Place holders for our windows so they don't get garbage collected.
|
||||
let mainWindow = null;
|
||||
|
||||
// Credentials that are fetched from the Keychain
|
||||
let credentials = [];
|
||||
|
||||
// Credentials helper window
|
||||
let credentialsWindow;
|
||||
|
||||
const template = [
|
||||
// { role: 'appMenu' }
|
||||
...(isMac ? [{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{role: 'about'},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Preferences', accelerator: 'CmdorCtrl+,', click: () => {
|
||||
credentialsWindow.show();
|
||||
}
|
||||
},
|
||||
{type: 'separator'},
|
||||
{role: 'hide'},
|
||||
{role: 'hideothers'},
|
||||
{role: 'unhide'},
|
||||
{type: 'separator'},
|
||||
{role: 'quit'}
|
||||
]
|
||||
}] : []),
|
||||
// { role: 'fileMenu' }
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
...(isMac ? [] : [{role: 'quit'}]),
|
||||
{
|
||||
label: 'Preferences', accelerator: 'CmdorCtrl+,', click: () => {
|
||||
credentialsWindow.show();
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
// { role: 'editMenu' }
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{role: 'undo'},
|
||||
{role: 'redo'},
|
||||
{type: 'separator'},
|
||||
{role: 'cut'},
|
||||
{role: 'copy'},
|
||||
{role: 'paste'},
|
||||
...(isMac ? [
|
||||
{role: 'pasteAndMatchStyle'},
|
||||
{role: 'delete'},
|
||||
{role: 'selectAll'},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Speech',
|
||||
submenu: [
|
||||
{role: 'startspeaking'},
|
||||
{role: 'stopspeaking'}
|
||||
]
|
||||
}
|
||||
] : [
|
||||
{role: 'delete'},
|
||||
{type: 'separator'},
|
||||
{role: 'selectAll'}
|
||||
])
|
||||
]
|
||||
},
|
||||
// { role: 'viewMenu' }
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{role: 'reload'},
|
||||
{role: 'forcereload'},
|
||||
{role: 'toggledevtools'},
|
||||
{type: 'separator'},
|
||||
{role: 'resetzoom'},
|
||||
{role: 'zoomin'},
|
||||
{role: 'zoomout'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen'},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Credentials Helper', accelerator: 'CmdorCtrl+k', click: () => {
|
||||
credentialsWindow.show();
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
// { role: 'windowMenu' }
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{role: 'minimize'},
|
||||
{role: 'zoom'},
|
||||
...(isMac ? [
|
||||
{type: 'separator'},
|
||||
{role: 'front'},
|
||||
{type: 'separator'},
|
||||
{role: 'window'}
|
||||
] : [
|
||||
{role: 'close'}
|
||||
])
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
click: async () => {
|
||||
const {shell} = require('electron')
|
||||
await shell.openExternal('https://joxit.dev/docker-registry-ui/')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
if (isMac) {
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
async function loadCredentials() {
|
||||
try {
|
||||
credentials = await keytar.findCredentials('docker-registry-ui');
|
||||
for (const credential of credentials) {
|
||||
// fix for windows
|
||||
credential.password = credential.password.replace(/\000+/g, '');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
credentials = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function createWindow() {
|
||||
return new Promise((resolve, reject) => {
|
||||
mainWindow = new BrowserWindow({
|
||||
height: 920,
|
||||
width: 1600,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
}
|
||||
});
|
||||
|
||||
if (isDevMode) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
mainWindow.setMenu(menu);
|
||||
}
|
||||
|
||||
mainWindow.loadURL(`file://${__dirname}/dist/index.html`);
|
||||
mainWindow.webContents.on('dom-ready', () => {
|
||||
console.log("Main Window DOM ready");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createCredentialsWindow() {
|
||||
return new Promise((resolve) => {
|
||||
credentialsWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 400,
|
||||
show: false,
|
||||
title: 'Credential Manager',
|
||||
parent: mainWindow,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (isDevMode) {
|
||||
credentialsWindow.openDevTools();
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
credentialsWindow.setMenu(null);
|
||||
}
|
||||
|
||||
credentialsWindow.loadURL(`file://${__dirname}/dist/authentication/index.html`);
|
||||
credentialsWindow.webContents.on('dom-ready', () => {
|
||||
console.log('Credentials Window DOM is ready');
|
||||
resolve();
|
||||
});
|
||||
|
||||
credentialsWindow.on('close', async (e) => {
|
||||
console.log("Closed credential window");
|
||||
credentialsWindow.hide();
|
||||
e.preventDefault();
|
||||
await loadCredentials();
|
||||
mainWindow.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.on('ready', async () => {
|
||||
await Promise.all([
|
||||
loadCredentials(),
|
||||
createWindow(),
|
||||
createCredentialsWindow(),
|
||||
]);
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
|
||||
app.on("login", (event, contents, authencation, info, callback) => {
|
||||
for (const credential of credentials) {
|
||||
const parsedUrl = url.parse(credential.account);
|
||||
if (parsedUrl.hostname === info.host) {
|
||||
return callback(parsedUrl.auth, credential.password);
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
39
examples/electron/package.json
Normal file
39
examples/electron/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "docker-registry-ui",
|
||||
"version": "1.4.8",
|
||||
"productName": "Registry UI",
|
||||
"description": "Electron Application for Docker Registry UI",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "electron ./",
|
||||
"start:dev": "parcel serve -d dist/authentication -t electron --public-url ./ authentication/index.html",
|
||||
"build": "parcel build -d dist/authentication -t electron --public-url ./ authentication/index.html",
|
||||
"rebuild": "electron-rebuild -f -w keytar",
|
||||
"package": "electron-packager --overwrite .",
|
||||
"sync": "copyfiles ../../dist/* ../../dist/**/* ./examples/out",
|
||||
"dist": "npm run rebuild && npm run sync && npm run build && npm run package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.52",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"keytar": "^5.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"electron": "^8.0.0",
|
||||
"electron-builder": "^22.6.0",
|
||||
"electron-packager": "^14.2.1",
|
||||
"electron-rebuild": "^1.10.1",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"keywords": [
|
||||
"electron"
|
||||
],
|
||||
"author": "",
|
||||
"license": "AGPL-3.0"
|
||||
}
|
||||
@@ -63,7 +63,7 @@ registry:
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: registry
|
||||
tag: 2.6.2
|
||||
tag: 2.7.1
|
||||
pullPolicy: Always
|
||||
probe:
|
||||
liveness: true
|
||||
|
||||
42
examples/issue-116/docker-compose.yml
Normal file
42
examples/issue-116/docker-compose.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
registry-srv:
|
||||
image: registry:2.7
|
||||
restart: always
|
||||
volumes:
|
||||
- storage:/var/lib/registry
|
||||
- ./htpasswd:/etc/docker/registry/htpasswd
|
||||
ports:
|
||||
- 5000:5000
|
||||
environment:
|
||||
- REGISTRY_HTTP_HEADERS_X-Content-Type-Options=[nosniff]
|
||||
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin=['http://localhost']
|
||||
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods=['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers=['Authorization']
|
||||
- REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers=['Docker-Content-Digest']
|
||||
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials=['true']
|
||||
- REGISTRY_AUTH_HTPASSWD_REALM=basic-realm
|
||||
- REGISTRY_AUTH_HTPASSWD_PATH=/etc/docker/registry/htpasswd
|
||||
- REGISTRY_STORAGE_DELETE_ENABLED=true
|
||||
networks:
|
||||
- registry-ui-net
|
||||
container_name: registry-srv
|
||||
|
||||
registry-ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
restart: always
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=Private Docker Registry
|
||||
- URL=http://localhost:5000
|
||||
- DELETE_IMAGES=true
|
||||
container_name: registry-ui
|
||||
|
||||
networks:
|
||||
registry-ui-net:
|
||||
|
||||
volumes:
|
||||
storage:
|
||||
driver: local
|
||||
3
examples/issue-116/htpasswd
Normal file
3
examples/issue-116/htpasswd
Normal file
@@ -0,0 +1,3 @@
|
||||
# login: registry
|
||||
# password: ui
|
||||
registry:$2y$11$1bmuJLK8HrQl5ACS/WeqRuJLUArUZfUcP2R23asmozEpfN76.pCHy
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
networks:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
networks:
|
||||
|
||||
@@ -28,7 +28,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://127.0.0.1']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -15,6 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['*']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Accept']
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
health:
|
||||
storagedriver:
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '2'
|
||||
|
||||
services:
|
||||
registry-srv:
|
||||
image: registry:latest
|
||||
image: registry:2.7
|
||||
restart: always
|
||||
volumes:
|
||||
- storage:/var/lib/registry
|
||||
@@ -20,8 +20,6 @@ services:
|
||||
- REGISTRY_TITLE=Private Docker Registry
|
||||
- REGISTRY_URL=http://registry-srv:5000
|
||||
- DELETE_IMAGES=true
|
||||
depends_on:
|
||||
- debugproxy
|
||||
networks:
|
||||
- registry-ui-net
|
||||
container_name: registry-ui
|
||||
|
||||
@@ -25,7 +25,7 @@ data:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['*']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
claimName: docker-registry
|
||||
containers:
|
||||
- name: registry
|
||||
image: "docker.io/registry:2.6.2"
|
||||
image: "docker.io/registry:2.7"
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: registry
|
||||
|
||||
9
examples/populate-registry
Executable file
9
examples/populate-registry
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
REGISTRY="${1:-localhost:5000}"
|
||||
|
||||
for image in alpine:latest alpine:edge alpine:3.11 alpine:3.10 alpine:3.9; do
|
||||
docker pull $image
|
||||
docker tag $image $REGISTRY/$image
|
||||
docker push $REGISTRY/$image
|
||||
done
|
||||
19
examples/proxy-headers/README.md
Normal file
19
examples/proxy-headers/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Set custom headers to the registry example
|
||||
|
||||
The interface and the docker registry will be accessible with <http://localhost>.
|
||||
|
||||
This example highlight the usage of custom headers when the UI is used as a proxy. When you wants to use a header name with hyphens, replace them by underscores in the variable. You can put headers in environment variable or in config file `/etc/nginx/.env`. They have the same writing style.
|
||||
|
||||
Headers can be useful in some cases such as avoid sending credentials when you are on the UI. Or give to the registry server other properties such as X-Forward-For header.
|
||||
|
||||
I will set these two headers in this example. X-Forward-For by environment variable and Authorization by file.
|
||||
|
||||
In order to set your credentials in the header, you need to know how [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) header works. Here we use the `Basic` authentication scheme, the credentials are constructed like this:
|
||||
- The username and the password are combined with a colon (`registry:ui`).
|
||||
- The resulting string is base64 encoded (`cmVnaXN0cnk6dWk=`). You can simply run `echo -n "registry:ui" | base64`.
|
||||
- In your header, put this value `Basic cmVnaXN0cnk6dWk=`
|
||||
- In your `/etc/nginx/.env`, the file will contains `NGINX_PROXY_HEADER_Authorization=Basic cmVnaXN0cnk6dWk=`
|
||||
|
||||
For X-Forward-For, replace all hyphens by underscores, and the value will be a nginx variable which is `$proxy_add_x_forwarded_for`. In your docker compose you will need to duplicate the `$` character. In your docker-compose, your environment will look like `NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for`
|
||||
|
||||
As usual, run the project with `docker-compose up -d` (for background mode)
|
||||
28
examples/proxy-headers/docker-compose.yml
Normal file
28
examples/proxy-headers/docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
- ./registry-config/credentials.yml:/etc/docker/registry/config.yml
|
||||
- ./registry-config/htpasswd:/etc/docker/registry/htpasswd
|
||||
networks:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- REGISTRY_URL=http://registry:5000
|
||||
- NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for
|
||||
volumes:
|
||||
- ./nginx.env:/etc/nginx/.env
|
||||
depends_on:
|
||||
- registry
|
||||
networks:
|
||||
- registry-ui-net
|
||||
|
||||
networks:
|
||||
registry-ui-net:
|
||||
1
examples/proxy-headers/nginx.env
Normal file
1
examples/proxy-headers/nginx.env
Normal file
@@ -0,0 +1 @@
|
||||
NGINX_PROXY_HEADER_Authorization=Basic cmVnaXN0cnk6dWk=
|
||||
25
examples/proxy-headers/registry-config/credentials.yml
Normal file
25
examples/proxy-headers/registry-config/credentials.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
version: 0.1
|
||||
log:
|
||||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
delete:
|
||||
enabled: true
|
||||
cache:
|
||||
blobdescriptor: inmemory
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
http:
|
||||
addr: :5000
|
||||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
auth:
|
||||
htpasswd:
|
||||
realm: basic-realm
|
||||
path: /etc/docker/registry/htpasswd
|
||||
1
examples/proxy-headers/registry-config/htpasswd
Normal file
1
examples/proxy-headers/registry-config/htpasswd
Normal file
@@ -0,0 +1 @@
|
||||
registry:$2y$11$1bmuJLK8HrQl5ACS/WeqRuJLUArUZfUcP2R23asmozEpfN76.pCHy
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.1'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- /opt/docker-registry:/var/lib/registry
|
||||
environment:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
- ./registry-config/credentials.yml:/etc/docker/registry/config.yml
|
||||
|
||||
@@ -15,7 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
networks:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
ports:
|
||||
- 5000:5000
|
||||
volumes:
|
||||
|
||||
@@ -15,7 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -15,7 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
ports:
|
||||
- 5000:5000
|
||||
volumes:
|
||||
|
||||
@@ -12,6 +12,8 @@ server {
|
||||
chunked_transfer_encoding on;
|
||||
# required for strict SNI checking: see Issue #70 (https://github.com/Joxit/docker-registry-ui/issues/70)
|
||||
proxy_ssl_server_name on;
|
||||
proxy_buffering off;
|
||||
proxy_ignore_headers "X-Accel-Buffering";
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
@@ -24,7 +26,7 @@ server {
|
||||
#! if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
|
||||
#! return 404;
|
||||
#! }
|
||||
#! proxy_set_header Host $http_host;
|
||||
#! ${NGINX_PROXY_HEADERS}
|
||||
#! proxy_pass ${REGISTRY_URL};
|
||||
#! }
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "docker-registry-ui",
|
||||
"version": "1.2.2",
|
||||
"version": "1.5.1",
|
||||
"scripts": {
|
||||
"build": "./node_modules/gulp/bin/gulp.js build"
|
||||
"build": "./node_modules/gulp/bin/gulp.js build",
|
||||
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -14,7 +15,7 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"del": "^3.0.0",
|
||||
"gulp": "^4.0.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-clean-css": "^4.2.0",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-filter": "^5.1.0",
|
||||
|
||||
BIN
src/fonts/Roboto-Bold.ttf
Normal file
BIN
src/fonts/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Bold.woff
Normal file
BIN
src/fonts/Roboto-Bold.woff
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Bold.woff2
Normal file
BIN
src/fonts/Roboto-Bold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Light.ttf
Normal file
BIN
src/fonts/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Light.woff
Normal file
BIN
src/fonts/Roboto-Light.woff
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Light.woff2
Normal file
BIN
src/fonts/Roboto-Light.woff2
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Regular.eot
Normal file
BIN
src/fonts/Roboto-Regular.eot
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Regular.ttf
Normal file
BIN
src/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Regular.woff
Normal file
BIN
src/fonts/Roboto-Regular.woff
Normal file
Binary file not shown.
BIN
src/fonts/Roboto-Regular.woff2
Normal file
BIN
src/fonts/Roboto-Regular.woff2
Normal file
Binary file not shown.
BIN
src/fonts/RobotoMono-Regular.eot
Normal file
BIN
src/fonts/RobotoMono-Regular.eot
Normal file
Binary file not shown.
BIN
src/fonts/RobotoMono-Regular.ttf
Normal file
BIN
src/fonts/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
BIN
src/fonts/RobotoMono-Regular.woff
Normal file
BIN
src/fonts/RobotoMono-Regular.woff
Normal file
Binary file not shown.
BIN
src/fonts/RobotoMono-Regular.woff2
Normal file
BIN
src/fonts/RobotoMono-Regular.woff2
Normal file
Binary file not shown.
@@ -25,8 +25,8 @@
|
||||
<!-- build:css style.css -->
|
||||
<link href="style.css" rel="stylesheet" type="text/css">
|
||||
<link href="material-icons.css" rel="stylesheet" type="text/css">
|
||||
<link href="roboto.css" rel="stylesheet" type="text/css">
|
||||
<!-- endbuild -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en" 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" />
|
||||
@@ -58,6 +58,8 @@
|
||||
<script src="tags/dialogs/menu.tag" type="riot/tag"></script>
|
||||
<script src="tags/image-size.tag" type="riot/tag"></script>
|
||||
<script src="tags/image-date.tag" type="riot/tag"></script>
|
||||
<script src="tags/image-content-digest.tag" type="riot/tag"></script>
|
||||
<script src="tags/pagination.tag" type="riot/tag"></script>
|
||||
<script src="tags/app.tag" type="riot/tag"></script>
|
||||
<script src="scripts/http.js"></script>
|
||||
<script src="scripts/script.js"></script>
|
||||
|
||||
52
src/roboto.css
Normal file
52
src/roboto.css
Normal file
@@ -0,0 +1,52 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(fonts/Roboto-Regular.eot); /* For IE6-8 */
|
||||
src: local('Roboto Light'),
|
||||
local('Roboto-Light'),
|
||||
url(fonts/Roboto-Light.woff2) format('woff2'),
|
||||
url(fonts/Roboto-Light.woff) format('woff'),
|
||||
url(fonts/Roboto-Light.ttf) format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(fonts/Roboto-Regular.eot); /* For IE6-8 */
|
||||
src: local('Roboto Regular'),
|
||||
local('Roboto-Regular'),
|
||||
url(fonts/Roboto-Regular.woff2) format('woff2'),
|
||||
url(fonts/Roboto-Regular.woff) format('woff'),
|
||||
url(fonts/Roboto-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(fonts/Roboto-Regular.eot); /* For IE6-8 */
|
||||
src: local('Roboto Bold'),
|
||||
local('Roboto-Bold'),
|
||||
url(fonts/Roboto-Bold.woff2) format('woff2'),
|
||||
url(fonts/Roboto-Bold.woff) format('woff'),
|
||||
url(fonts/Roboto-Bold.ttf) format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(fonts/RobotoMono-Regular.eot); /* For IE6-8 */
|
||||
src: local('Roboto Mono Regular'),
|
||||
local('RobotoMono-Regular'),
|
||||
local('Roboto-Mono-Regular'),
|
||||
url(fonts/RobotoMono-Regular.woff2) format('woff2'),
|
||||
url(fonts/RobotoMono-Regular.woff) format('woff'),
|
||||
url(fonts/RobotoMono-Regular.ttf) format('truetype');
|
||||
}
|
||||
@@ -22,6 +22,30 @@ function Http() {
|
||||
this._headers = {};
|
||||
}
|
||||
|
||||
Http.prototype.getContentDigest = function(cb) {
|
||||
if (this.oReq.hasHeader('Docker-Content-Digest')) {
|
||||
// Same origin or advanced CORS headers set:
|
||||
// 'Access-Control-Expose-Headers: Docker-Content-Digest'
|
||||
cb(this.oReq.getResponseHeader('Docker-Content-Digest'))
|
||||
} else if (window.crypto && window.TextEncoder) {
|
||||
crypto.subtle.digest(
|
||||
'SHA-256',
|
||||
new TextEncoder().encode(this.oReq.responseText)
|
||||
).then(function (buffer) {
|
||||
cb(
|
||||
'sha256:' + Array.from(
|
||||
new Uint8Array(buffer)
|
||||
).map(function(byte) {
|
||||
return byte.toString(16).padStart(2, '0');
|
||||
}).join('')
|
||||
);
|
||||
})
|
||||
} else {
|
||||
// IE and old Edge
|
||||
// simply do not call the callback and skip the setup downstream
|
||||
}
|
||||
};
|
||||
|
||||
Http.prototype.addEventListener = function(e, f) {
|
||||
this._events[e] = f;
|
||||
const self = this;
|
||||
@@ -41,6 +65,7 @@ Http.prototype.addEventListener = function(e, f) {
|
||||
req.withCredentials = true;
|
||||
req.hasHeader = Http.hasHeader;
|
||||
req.getErrorMessage = Http.getErrorMessage;
|
||||
self.oReq = req;
|
||||
req.send();
|
||||
} else {
|
||||
f.bind(this)();
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
var registryUI = {}
|
||||
registryUI.URL_QUERY_PARAM_REGEX = /[&?]url=/;
|
||||
registryUI.URL_PARAM_REGEX = /^url=/;
|
||||
registryUI.showContentDigest = true;
|
||||
registryUI.catalogElementsLimit = 100000;
|
||||
|
||||
registryUI.name = registryUI.url = function(byPassQueryParam) {
|
||||
registryUI.url = function(byPassQueryParam) {
|
||||
if (!registryUI._url) {
|
||||
const url = registryUI.getUrlQueryParam();
|
||||
if (url) {
|
||||
@@ -33,6 +35,9 @@ registryUI.name = registryUI.url = function(byPassQueryParam) {
|
||||
}
|
||||
return registryUI._url;
|
||||
}
|
||||
registryUI.name = function() {
|
||||
return registryUI.stripHttps(registryUI.url());
|
||||
}
|
||||
registryUI.getRegistryServer = function(i) {
|
||||
try {
|
||||
const res = JSON.parse(localStorage.getItem('registryServer'));
|
||||
@@ -85,7 +90,7 @@ registryUI.removeServer = function(url) {
|
||||
}
|
||||
|
||||
registryUI.updateHistory = function(url) {
|
||||
history.pushState(null, '', (url ? '?url=' + registryUI.encodeURI(url) : '?') + window.location.hash);
|
||||
registryUI.updateQueryString({ url: registryUI.encodeURI(url) })
|
||||
registryUI._url = url;
|
||||
}
|
||||
|
||||
@@ -100,10 +105,12 @@ registryUI.getUrlQueryParam = function () {
|
||||
};
|
||||
|
||||
registryUI.encodeURI = function(url) {
|
||||
if (!url) { return; }
|
||||
return url.indexOf('&') < 0 ? window.encodeURIComponent(url) : btoa(url);
|
||||
};
|
||||
|
||||
registryUI.decodeURI = function(url) {
|
||||
if (!url) { return; }
|
||||
return url.startsWith('http') ? window.decodeURIComponent(url) : atob(url);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,10 +24,17 @@ registryUI.url = function() {
|
||||
return url;
|
||||
};
|
||||
registryUI.name = function() {
|
||||
return '${REGISTRY_TITLE}' || registryUI.url();
|
||||
const name = '${REGISTRY_TITLE}';
|
||||
if (name) {
|
||||
// the user can strip the http prefix if they wish
|
||||
return name;
|
||||
}
|
||||
return registryUI.stripHttps(registryUI.url());
|
||||
};
|
||||
registryUI.pullUrl = '${PULL_URL}';
|
||||
registryUI.isImageRemoveActivated = true;
|
||||
registryUI.showContentDigest = true;
|
||||
registryUI.catalogElementsLimit = 100000;
|
||||
registryUI.catalog = {};
|
||||
registryUI.taglist = {};
|
||||
registryUI.taghistory = {};
|
||||
|
||||
@@ -65,4 +65,63 @@ registryUI.getHistoryIcon = function(attribute) {
|
||||
default:
|
||||
''
|
||||
}
|
||||
}
|
||||
|
||||
registryUI.getPage = function(elts, page, limit) {
|
||||
if (!limit) { limit = 100; }
|
||||
if (!elts) { return []; }
|
||||
return elts.slice((page - 1) * limit, limit * page);
|
||||
}
|
||||
|
||||
registryUI.getNumPages = function(elts, limit) {
|
||||
if (!limit) { limit = 100; }
|
||||
if (!elts) { return 0; }
|
||||
return Math.trunc(elts.length / limit) + 1;
|
||||
}
|
||||
|
||||
registryUI.getPageLabels = function(page, nPages) {
|
||||
var pageLabels = [];
|
||||
var maxItems = 10;
|
||||
if (nPages === 1) { return pageLabels; }
|
||||
if (page !== 1 && nPages >= maxItems) {
|
||||
pageLabels.push({'icon': 'first_page', page: 1});
|
||||
pageLabels.push({'icon': 'chevron_left', page: page - 1});
|
||||
}
|
||||
var start = Math.round(Math.max(1, Math.min(page - maxItems / 2, nPages - maxItems + 1)));
|
||||
for (var i = start; i < Math.min(nPages + 1, start + maxItems); i++) {
|
||||
pageLabels.push({
|
||||
page: i,
|
||||
current: i === page,
|
||||
'space-left': page === 1 && nPages > maxItems,
|
||||
'space-right': page === nPages && nPages > maxItems
|
||||
});
|
||||
}
|
||||
if (page !== nPages && nPages >= maxItems) {
|
||||
pageLabels.push({'icon': 'chevron_right', page: page + 1});
|
||||
pageLabels.push({'icon': 'last_page', page: nPages});
|
||||
}
|
||||
return pageLabels;
|
||||
}
|
||||
|
||||
registryUI.updateQueryString = function(qs) {
|
||||
var search = '';
|
||||
for (var key in qs) {
|
||||
if (qs[key] !== undefined) {
|
||||
search += (search.length > 0 ? '&' : '?') +key + '=' + qs[key];
|
||||
}
|
||||
}
|
||||
history.pushState(null, '', search + window.location.hash);
|
||||
}
|
||||
|
||||
registryUI.stripHttps = function (url) {
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
return url.replace(/^https?:\/\//, '');
|
||||
};
|
||||
|
||||
registryUI.eventTransfer = function(from, to) {
|
||||
from.on('*', function(event, param) {
|
||||
to.trigger(event, param);
|
||||
})
|
||||
}
|
||||
139
src/style.css
139
src/style.css
@@ -48,21 +48,54 @@ main {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
material-card {
|
||||
min-height: 200px;
|
||||
max-width: 75%;
|
||||
material-card, material-tabs, pagination .conatianer {
|
||||
max-width: 95%;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 950px){
|
||||
material-card {
|
||||
width: 95%;
|
||||
max-width: 750px;
|
||||
pagination .conatianer {
|
||||
display: flex;
|
||||
display: -moz-flex;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
}
|
||||
|
||||
pagination .conatianer .pagination-centered {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* 1515px * 0.95 = 1440px */
|
||||
@media screen and (min-width: 1515px){
|
||||
material-card, material-tabs, pagination .conatianer {
|
||||
max-width: 1440px;
|
||||
}
|
||||
}
|
||||
|
||||
material-tabs {
|
||||
display: block;
|
||||
-webkit-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16), 0 2px 10px 0 rgba(0,0,0,.12);
|
||||
-ms-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
-moz-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
-o-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16), 0 2px 10px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
material-tabs material-button,
|
||||
material-tabs material-button .content .text {
|
||||
background-color: #fff;
|
||||
color: #aaa;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
material-tabs .line-wrapper .line {
|
||||
background-color: #25313b;
|
||||
}
|
||||
material-tabs material-button.selected .content .text {
|
||||
color: #25313b;
|
||||
}
|
||||
|
||||
material-spinner {
|
||||
align-self: center;
|
||||
}
|
||||
@@ -95,6 +128,7 @@ h2 {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.material-card-title-action h2 .source-hint,
|
||||
.material-card-title-action h2 .item-count {
|
||||
font-size: 0.7em;
|
||||
margin-left: 1em;
|
||||
@@ -212,12 +246,14 @@ material-card table th {
|
||||
}
|
||||
|
||||
material-card material-button:hover,
|
||||
material-card table tbody tr:hover {
|
||||
material-card table tbody tr:hover,
|
||||
pagination material-button:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
material-card material-button,
|
||||
material-card table tbody tr {
|
||||
material-card table tbody tr,
|
||||
pagination material-button {
|
||||
transition-duration: .28s;
|
||||
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
|
||||
transition-property: background-color;
|
||||
@@ -269,6 +305,11 @@ material-button .content i.material-icons,
|
||||
color: #777;
|
||||
}
|
||||
|
||||
material-button[disabled] .content i.material-icons,
|
||||
material-checkbox[disabled] .content i.material-icons {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
material-snackbar .toast {
|
||||
height: auto;
|
||||
}
|
||||
@@ -326,7 +367,8 @@ dropdown-item, #menu-control-dropdown p {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
material-popup material-button {
|
||||
material-popup material-button,
|
||||
pagination material-button {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
@@ -357,6 +399,10 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
material-footer {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 760px) and (max-height: 750px) {
|
||||
main {
|
||||
min-height: calc(100% - 144px - 2.5em);
|
||||
@@ -385,7 +431,27 @@ select {
|
||||
}
|
||||
|
||||
.copy-to-clipboard {
|
||||
padding: 12px 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
#image-tag-header {
|
||||
/* spacing for clipboard + default th spacing */
|
||||
/* 5 + 2 + 3 + 24 + 3 + 2 + 18 */
|
||||
padding-right: 57px;
|
||||
}
|
||||
image-tag, .copy-to-clipboard {
|
||||
display: inline-block;
|
||||
}
|
||||
image-content-digest {
|
||||
display: none;
|
||||
font-family: 'Roboto Mono', 'Roboto', 'Helvetica', 'Arial', sans-serif !important;
|
||||
}
|
||||
@media screen and (min-width: 1024px) {
|
||||
#image-content-digest-header {
|
||||
padding-right: 57px;
|
||||
}
|
||||
image-content-digest {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.show-tag-history {
|
||||
@@ -403,6 +469,14 @@ select {
|
||||
padding: 7px 5px;
|
||||
}
|
||||
|
||||
taglist .creation-date {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
taglist .image-size {
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
catalog material-card,
|
||||
tag-history material-card {
|
||||
min-height: auto;
|
||||
@@ -423,6 +497,7 @@ tag-history-element {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
min-width: 100px;
|
||||
min-height: 3em;
|
||||
width: 420px;
|
||||
float: left;
|
||||
overflow-x: auto;
|
||||
@@ -445,7 +520,8 @@ tag-history-button button {
|
||||
border: none;
|
||||
}
|
||||
|
||||
material-card material-button {
|
||||
material-card material-button,
|
||||
pagination material-button {
|
||||
max-height: 30px;
|
||||
max-width: 30px;
|
||||
}
|
||||
@@ -454,7 +530,8 @@ material-button:hover material-waves {
|
||||
background: none;
|
||||
}
|
||||
|
||||
material-card material-button {
|
||||
material-card material-button,
|
||||
pagination material-button {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
@@ -475,6 +552,16 @@ catalog-element catalog-element.hide material-card {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
catalog-element catalog-element > .content {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1515px){
|
||||
catalog-element catalog-element > .content material-card {
|
||||
max-width: calc(1440px - 3em);
|
||||
}
|
||||
}
|
||||
|
||||
remove-image {
|
||||
width: 30px;
|
||||
}
|
||||
@@ -506,4 +593,30 @@ material-checkbox .checkbox {
|
||||
|
||||
material-checkbox .checkbox.checked {
|
||||
background-color: #777;
|
||||
}
|
||||
|
||||
pagination material-button {
|
||||
padding: 0.2em 0.75em;
|
||||
}
|
||||
|
||||
pagination material-button .content {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
pagination material-button.current {
|
||||
border: 1px solid rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
pagination material-button.current.space-left {
|
||||
margin-left: 85px;
|
||||
}
|
||||
|
||||
pagination material-button.current.space-right {
|
||||
margin-right: 85px;
|
||||
}
|
||||
|
||||
pagination material-button .content i.material-icons {
|
||||
color: #000;
|
||||
}
|
||||
@@ -92,12 +92,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
registryUI.errorSnackbar = function(message) {
|
||||
return registryUI.snackbar(message, true);
|
||||
};
|
||||
registryUI.showErrorCanNotReadContentDigest = function() {
|
||||
registryUI.errorSnackbar(
|
||||
'Access on registry response was blocked. Try adding the header ' +
|
||||
'`Access-Control-Expose-Headers: Docker-Content-Digest`' +
|
||||
' to your proxy or registry: ' +
|
||||
'https://docs.docker.com/registry/configuration/#http'
|
||||
);
|
||||
};
|
||||
registryUI.cleanName = function() {
|
||||
const url = registryUI.pullUrl || (registryUI.url() && registryUI.url().length > 0 && registryUI.url()) || window.location.host;
|
||||
if (url) {
|
||||
return url.startsWith('http') ? url.replace(/https?:\/\//, '') : url;
|
||||
}
|
||||
return '';
|
||||
return registryUI.stripHttps(url);
|
||||
};
|
||||
route.parser(null, function(path, filter) {
|
||||
const f = filter
|
||||
@@ -113,9 +118,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return char >= '0' && char <= '9';
|
||||
};
|
||||
|
||||
registryUI.DockerImage = function(name, tag) {
|
||||
registryUI.DockerImage = function(name, tag, list) {
|
||||
this.name = name;
|
||||
this.tag = tag;
|
||||
this.list = list;
|
||||
this.chars = 0;
|
||||
riot.observable(this);
|
||||
this.on('get-size', function() {
|
||||
if (this.size !== undefined) {
|
||||
@@ -130,8 +137,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return this.fillInfo();
|
||||
});
|
||||
this.on('get-date', function() {
|
||||
if (this.date !== undefined) {
|
||||
return this.trigger('date', this.date);
|
||||
if (this.creationDate !== undefined) {
|
||||
return this.trigger('creation-date', this.creationDate);
|
||||
}
|
||||
return this.fillInfo();
|
||||
});
|
||||
this.on('content-digest-chars', function (chars) {
|
||||
this.chars = chars;
|
||||
});
|
||||
this.on('get-content-digest-chars', function() {
|
||||
return this.trigger('content-digest-chars', this.chars);
|
||||
});
|
||||
this.on('get-content-digest', function() {
|
||||
if (this.digest !== undefined) {
|
||||
return this.trigger('content-digest', this.digest);
|
||||
}
|
||||
return this.fillInfo();
|
||||
});
|
||||
@@ -174,6 +193,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
oReq.addEventListener('loadend', function() {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
const response = JSON.parse(this.responseText);
|
||||
if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') {
|
||||
self.trigger('list', response);
|
||||
const manifest = response.manifests[0];
|
||||
const image = new registryUI.DockerImage(self.name, manifest.digest)
|
||||
registryUI.eventTransfer(image, self)
|
||||
image.fillInfo()
|
||||
self.variants = [image];
|
||||
return;
|
||||
}
|
||||
self.size = response.layers.reduce(function(acc, e) {
|
||||
return acc + e.size;
|
||||
}, 0);
|
||||
@@ -181,6 +209,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
self.layers = response.layers;
|
||||
self.trigger('size', self.size);
|
||||
self.trigger('sha256', self.sha256);
|
||||
oReq.getContentDigest(function (digest) {
|
||||
self.digest = digest;
|
||||
self.trigger('content-digest', digest);
|
||||
if (!digest) {
|
||||
registryUI.showErrorCanNotReadContentDigest();
|
||||
}
|
||||
});
|
||||
self.getBlobs(response.config.digest)
|
||||
} else if (this.status == 404) {
|
||||
registryUI.errorSnackbar('Manifest for ' + self.name + ':' + self.tag + ' not found');
|
||||
@@ -189,7 +224,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
});
|
||||
oReq.open('GET', registryUI.url() + '/v2/' + self.name + '/manifests/' + self.tag);
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json'
|
||||
+ (self.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : ''));
|
||||
oReq.send();
|
||||
};
|
||||
|
||||
@@ -217,7 +253,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
});
|
||||
oReq.open('GET', registryUI.url() + '/v2/' + self.name + '/blobs/' + blob);
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
||||
oReq.send();
|
||||
};
|
||||
|
||||
@@ -225,6 +261,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
route('taglist/' + image);
|
||||
};
|
||||
|
||||
registryUI.getPageQueryParam = function() {
|
||||
var qs = route.query();
|
||||
try {
|
||||
return qs.page !== undefined ? parseInt(qs.page.replace(/#.*/, '')) : 1;
|
||||
} catch(e) { return 1; }
|
||||
}
|
||||
|
||||
registryUI.getQueryParams = function(update) {
|
||||
var qs = route.query();
|
||||
update = update || {};
|
||||
for (var key in qs) {
|
||||
if (qs[key] !== undefined) {
|
||||
qs[key] = qs[key].replace(/#!.*/, '');
|
||||
} else {
|
||||
delete qs[key];
|
||||
}
|
||||
}
|
||||
for (var key in update) {
|
||||
if (update[key] !== undefined) {
|
||||
qs[key] = update[key];
|
||||
}
|
||||
}
|
||||
return qs;
|
||||
}
|
||||
route.start(true);
|
||||
</script>
|
||||
</app>
|
||||
@@ -16,18 +16,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<catalog-element>
|
||||
<!-- Begin of tag -->
|
||||
<material-card class="list highlight" item="{item}" expanded="{expanded}">
|
||||
<material-waves onmousedown="{launch}" center="true" color="#ddd" />
|
||||
<span>
|
||||
<i class="material-icons">send</i>
|
||||
{ typeof opts.item === "string" ? opts.item : opts.item.repo }
|
||||
<div hide="{typeof opts.item === "string"}" class="item-count right">
|
||||
{ opts.item.images && opts.item.images.length } images
|
||||
<i class="material-icons animated {expanded: opts.expanded}">expand_more</i>
|
||||
</div>
|
||||
</span>
|
||||
</material-card>
|
||||
<catalog-element hide="{typeof opts.item === "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
|
||||
<div class="content">
|
||||
<material-card class="list highlight" item="{item}" expanded="{expanded}">
|
||||
<material-waves onmousedown="{launch}" center="true" color="#ddd" />
|
||||
<span>
|
||||
<i class="material-icons">send</i>
|
||||
{ typeof opts.item === "string" ? opts.item : opts.item.repo }
|
||||
<div if="{typeof opts.item !== "string"}" class="item-count right">
|
||||
{ opts.item.images && opts.item.images.length } images
|
||||
<i class="material-icons animated {expanded: opts.expanded}">expand_more</i>
|
||||
</div>
|
||||
</span>
|
||||
</material-card>
|
||||
<catalog-element if="{typeof opts.item !== "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
|
||||
</div>
|
||||
<script>
|
||||
this.on('mount', function() {
|
||||
const self = this;
|
||||
|
||||
@@ -16,17 +16,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<catalog>
|
||||
<!-- Begin of tag -->
|
||||
<material-card ref="catalog-tag" class="catalog">
|
||||
<material-card ref="catalog-tag" class="catalog header">
|
||||
<div class="material-card-title-action">
|
||||
<h2>
|
||||
Repositories of { registryUI.name() }
|
||||
<div class="item-count">{ registryUI.catalog.length } images</div>
|
||||
</h2>
|
||||
</div>
|
||||
<div hide="{ registryUI.catalog.loadend }" class="spinner-wrapper">
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
</material-card>
|
||||
<div hide="{ registryUI.catalog.loadend }" class="spinner-wrapper">
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
<catalog-element each="{ item in registryUI.catalog.repositories }" />
|
||||
<script>
|
||||
registryUI.catalog.instance = this;
|
||||
@@ -36,6 +36,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
oReq.addEventListener('load', function() {
|
||||
registryUI.catalog.repositories = [];
|
||||
if (this.status == 200) {
|
||||
if (!registryUI.url()) {
|
||||
registryUI._url = window.location.origin + window.location.pathname.replace(/\/+$/, '')
|
||||
}
|
||||
registryUI.catalog.repositories = JSON.parse(this.responseText).repositories || [];
|
||||
registryUI.catalog.repositories.sort();
|
||||
registryUI.catalog.length = registryUI.catalog.repositories.length; registryUI.catalog.repositories = registryUI.catalog.repositories.reduce(function(acc, e) {
|
||||
@@ -65,7 +68,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
registryUI.catalog.loadend = true;
|
||||
registryUI.catalog.instance.update();
|
||||
});
|
||||
oReq.open('GET', registryUI.url() + '/v2/_catalog?n=100000');
|
||||
oReq.open('GET', registryUI.url() + '/v2/_catalog?n=' + registryUI.catalogElementsLimit);
|
||||
oReq.send();
|
||||
};
|
||||
registryUI.catalog.display();
|
||||
|
||||
@@ -15,13 +15,29 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<copy-to-clipboard>
|
||||
<div class="copy-to-clipboard">
|
||||
<input ref="input" style="display: none; width: 1px; height: 1px;" value="{ this.dockerCmd }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onclick="{ this.copy }" title="Copy pull command.">
|
||||
<i class="material-icons">content_copy</i>
|
||||
</material-button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
this.dockerCmd = 'docker pull ' + registryUI.cleanName() + '/' + opts.image.name + ':' + opts.image.tag;
|
||||
this.prefix = 'docker pull ' + registryUI.cleanName() + '/' + opts.image.name;
|
||||
const self = this;
|
||||
if (opts.target === 'tag') {
|
||||
self.dockerCmd = self.prefix + ':' + opts.image.tag;
|
||||
} else {
|
||||
opts.image.one('content-digest', function (digest) {
|
||||
self.dockerCmd = self.prefix + '@' + digest;
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
}
|
||||
|
||||
this.copy = function () {
|
||||
if (!self.dockerCmd) {
|
||||
registryUI.showErrorCanNotReadContentDigest();
|
||||
return;
|
||||
}
|
||||
const copyText = this.refs['input'];
|
||||
copyText.style.display = 'block';
|
||||
copyText.select();
|
||||
|
||||
48
src/tags/image-content-digest.tag
Normal file
48
src/tags/image-content-digest.tag
Normal file
@@ -0,0 +1,48 @@
|
||||
<!--
|
||||
Copyright (C) 2019 Jakob Ackermann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<image-content-digest>
|
||||
<div title="{ this.title }">{ this.display_id }</div>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
self.chars = -1;
|
||||
|
||||
self.onResize = function(chars) {
|
||||
if (chars === self.chars) {
|
||||
return;
|
||||
}
|
||||
self.chars = chars;
|
||||
if (chars >= 70) {
|
||||
self.display_id = self.digest;
|
||||
self.title = '';
|
||||
} else if (chars <= 0) {
|
||||
self.display_id = '';
|
||||
self.title = self.digest;
|
||||
} else {
|
||||
self.display_id = self.digest.slice(0, chars) + '...';
|
||||
self.title = self.digest;
|
||||
}
|
||||
self.update();
|
||||
};
|
||||
|
||||
opts.image.one('content-digest', function(digest) {
|
||||
self.digest = digest;
|
||||
opts.image.on('content-digest-chars', self.onResize);
|
||||
opts.image.trigger('get-content-digest-chars');
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
</script>
|
||||
</image-content-digest>
|
||||
@@ -24,5 +24,7 @@
|
||||
self.localDate = date.toLocaleString()
|
||||
self.update();
|
||||
});
|
||||
|
||||
opts.image.trigger('get-date');
|
||||
</script>
|
||||
</image-date>
|
||||
39
src/tags/pagination.tag
Normal file
39
src/tags/pagination.tag
Normal file
@@ -0,0 +1,39 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<pagination>
|
||||
<!-- Begin of tag -->
|
||||
<div class="conatianer">
|
||||
<div class="pagination-centered">
|
||||
<material-button waves-color="rgba(158,158,158,.4)" each="{p in this.opts.pages}" class="{ current: p.current, space-left: p['space-left'], space-right: p['space-right']}">
|
||||
<i show="{ p.icon }" class="material-icons">{ p.icon }</i>
|
||||
<div hide="{ p.icon }">{ p.page }</div>
|
||||
</material-button>
|
||||
</div>
|
||||
<div>
|
||||
<script>
|
||||
this.on('updated', function() {
|
||||
if (!this.tags['material-button']) { return; }
|
||||
var buttons = Array.isArray(this.tags['material-button']) ? this.tags['material-button'] : [this.tags['material-button']];
|
||||
buttons.forEach(function(button) {
|
||||
button.root.onclick = function() {
|
||||
registryUI.taglist.instance.trigger('page-update', button.p.page)
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!-- End of tag -->
|
||||
</pagination>
|
||||
@@ -15,63 +15,67 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<remove-image>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." hide="{ opts.multiDelete }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." if="{ !opts.multiDelete }" disabled="{ !this.digest }">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<material-checkbox show="{ opts.multiDelete }" title="Select this tag to delete it."></material-checkbox>
|
||||
<material-checkbox if="{ opts.multiDelete }" title="Select this tag to delete it." disabled="{ !this.digest }"></material-checkbox>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
|
||||
this.on('update', function() {
|
||||
if (!this.opts.multiDelete && this.tags['material-checkbox'].checked) {
|
||||
this.tags['material-checkbox'].toggle();
|
||||
this.on('updated', function() {
|
||||
});
|
||||
|
||||
this.on('updated', function() {
|
||||
if (self.multiDelete == self.opts.multiDelete) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this.on('mount', function() {
|
||||
this.delete = this.tags['material-button'].root.onclick = function(ignoreError) {
|
||||
const name = self.opts.image.name;
|
||||
const tag = self.opts.image.tag;
|
||||
const oReq = new Http();
|
||||
oReq.addEventListener('loadend', function() {
|
||||
if (self.tags['material-button']) {
|
||||
self.delete = function(ignoreError) {
|
||||
const name = self.opts.image.name;
|
||||
const tag = self.opts.image.tag;
|
||||
registryUI.taglist.go(name);
|
||||
if (this.status == 200) {
|
||||
if (!this.hasHeader('Docker-Content-Digest')) {
|
||||
registryUI.errorSnackbar('You need to add Access-Control-Expose-Headers: [\'Docker-Content-Digest\'] in your server configuration.');
|
||||
return;
|
||||
}
|
||||
const digest = this.getResponseHeader('Docker-Content-Digest');
|
||||
const oReq = new Http();
|
||||
oReq.addEventListener('loadend', function() {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
registryUI.taglist.display()
|
||||
registryUI.snackbar('Deleting ' + name + ':' + tag + ' image. Run `registry garbage-collect config.yml` on your registry');
|
||||
} else if (this.status == 404) {
|
||||
ignoreError || registryUI.errorSnackbar('Digest not found');
|
||||
} else {
|
||||
registryUI.snackbar(this.responseText);
|
||||
}
|
||||
});
|
||||
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + digest);
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
|
||||
oReq.addEventListener('error', function() {
|
||||
registryUI.errorSnackbar('An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].');
|
||||
});
|
||||
oReq.send();
|
||||
} else if (this.status == 404) {
|
||||
registryUI.errorSnackbar('Manifest for ' + name + ':' + tag + ' not found');
|
||||
} else {
|
||||
registryUI.snackbar(this.responseText);
|
||||
if (!self.digest) {
|
||||
registryUI.snackbar('Information for ' + name + ':' + tag + ' are not yet loaded.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
oReq.open('HEAD', registryUI.url() + '/v2/' + name + '/manifests/' + tag);
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
|
||||
oReq.send();
|
||||
};
|
||||
const oReq = new Http();
|
||||
oReq.addEventListener('loadend', function() {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
registryUI.taglist.display()
|
||||
registryUI.snackbar('Deleting ' + name + ':' + tag + ' image. Run `registry garbage-collect config.yml` on your registry');
|
||||
} else if (this.status == 404) {
|
||||
ignoreError || registryUI.errorSnackbar('Digest not found for this image in your registry.');
|
||||
} else {
|
||||
registryUI.snackbar(this.responseText);
|
||||
}
|
||||
});
|
||||
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + self.digest);
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
||||
oReq.addEventListener('error', function() {
|
||||
registryUI.errorSnackbar('An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].');
|
||||
});
|
||||
oReq.send();
|
||||
};
|
||||
self.tags['material-button'].root.onclick = function() {
|
||||
self.delete();
|
||||
}
|
||||
}
|
||||
|
||||
this.tags['material-checkbox'].on('toggle', function() {
|
||||
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
|
||||
});
|
||||
if (self.tags['material-checkbox']) {
|
||||
if (!self.opts.multiDelete && self.tags['material-checkbox'].checked) {
|
||||
self.tags['material-checkbox'].toggle();
|
||||
}
|
||||
self.tags['material-checkbox'].on('toggle', function() {
|
||||
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
|
||||
});
|
||||
}
|
||||
self.multiDelete = self.opts.multiDelete;
|
||||
});
|
||||
|
||||
opts.image.one('content-digest', function(digest) {
|
||||
self.digest = digest;
|
||||
self.update();
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
</script>
|
||||
</remove-image>
|
||||
|
||||
@@ -22,7 +22,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
this.on('mount', function() {
|
||||
const self = this;
|
||||
this.refs.button.root.onclick = function() {
|
||||
registryUI.taghistory._image = self.opts.image;
|
||||
registryUI.taghistory.go(self.opts.image.name, self.opts.image.tag);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-history>
|
||||
<material-card ref="tag-history-tag" class="tag-history">
|
||||
<material-card ref="tag-history-tag" class="tag-history header">
|
||||
<div class="material-card-title-action">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
@@ -26,46 +26,48 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
</material-card>
|
||||
<div hide="{ registryUI.taghistory.loadend }" class="spinner-wrapper">
|
||||
<material-spinner/>
|
||||
<material-spinner />
|
||||
</div>
|
||||
|
||||
<material-tabs if="{ this.archs }" useLine="true" tabs="{ this.archs }" tabchanged="{ this.tabchanged }" />
|
||||
|
||||
<material-card each="{ guiElement in this.elements }" class="tag-history-element">
|
||||
<tag-history-element each="{ entry in guiElement }" if="{ entry.value && entry.value.length > 0}"/>
|
||||
<tag-history-element each="{ entry in guiElement }" if="{ entry.value && entry.value.length > 0}" />
|
||||
</material-card>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
const eltIdx = function(e) {
|
||||
const eltIdx = function (e) {
|
||||
switch (e) {
|
||||
case 'id': return 1;
|
||||
case 'created': return 2;
|
||||
case 'created_by': return 3;
|
||||
case 'size': return 4;
|
||||
case 'os': return 5;
|
||||
case 'architecture': return 6;
|
||||
case 'created': return 1;
|
||||
case 'created_by': return 2;
|
||||
case 'size': return 3;
|
||||
case 'os': return 4;
|
||||
case 'architecture': return 5;
|
||||
case 'id': return 6;
|
||||
case 'linux': return 7;
|
||||
case 'docker_version': return 8;
|
||||
default: return 10;
|
||||
}
|
||||
};
|
||||
|
||||
const eltSort = function(e1, e2) {
|
||||
const eltSort = function (e1, e2) {
|
||||
return eltIdx(e1.key) - eltIdx(e2.key);
|
||||
};
|
||||
|
||||
const modifySpecificAttributeTypes = function(attribute, value) {
|
||||
const modifySpecificAttributeTypes = function (attribute, value) {
|
||||
switch (attribute) {
|
||||
case 'created':
|
||||
return new Date(value).toLocaleString();
|
||||
case 'created_by':
|
||||
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
|
||||
return (cmd && cmd [1]) || 'RUN'
|
||||
return (cmd && cmd[1]) || 'RUN'
|
||||
case 'size':
|
||||
return registryUI.bytesToSize(value);
|
||||
case 'Entrypoint':
|
||||
case 'Cmd':
|
||||
return (value || []).join(' ');
|
||||
case 'Labels':
|
||||
return Object.keys(value || {}).map(function(elt) {
|
||||
return Object.keys(value || {}).map(function (elt) {
|
||||
return value[elt] ? elt + '=' + value[elt] : '';
|
||||
});
|
||||
case 'Volumes':
|
||||
@@ -75,14 +77,17 @@ 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) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const getConfig = function (blobs) {
|
||||
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels', 'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts']
|
||||
.reduce(function (acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value && e === 'architecture' && blobs.variant) {
|
||||
acc[e] = value + blobs.variant;
|
||||
} else if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!res.author && (res.Labels && res.Labels.maintainer)) {
|
||||
res.author = blobs.config.Labels.maintainer;
|
||||
@@ -92,10 +97,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return res;
|
||||
};
|
||||
|
||||
const processBlobs = function(blobs) {
|
||||
const processBlobs = function (blobs) {
|
||||
function exec(elt) {
|
||||
const guiElements = [];
|
||||
for (const attribute in elt) {
|
||||
for (var attribute in elt) {
|
||||
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
|
||||
const value = elt[attribute];
|
||||
const guiElement = {
|
||||
@@ -107,27 +112,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
return guiElements.sort(eltSort);
|
||||
}
|
||||
|
||||
self.elements.push(exec(getConfig(blobs)));
|
||||
blobs.history.reverse().forEach(function(elt) { self.elements.push(exec(elt)) });
|
||||
self.elements = new Array(blobs.history.length + 1);
|
||||
self.elements[0] = exec(getConfig(blobs));
|
||||
blobs.history.forEach(function (elt, i) { self.elements[blobs.history.length - i] = exec(elt) });
|
||||
registryUI.taghistory.loadend = true;
|
||||
self.update();
|
||||
};
|
||||
|
||||
registryUI.taghistory.display = function() {
|
||||
self.elements = []
|
||||
const blobs = registryUI.taghistory._image && registryUI.taghistory._image.blobs;
|
||||
if (blobs) {
|
||||
window.scrollTo(0, 0);
|
||||
return processBlobs(blobs);
|
||||
}
|
||||
const image = new registryUI.DockerImage(registryUI.taghistory.image, registryUI.taghistory.tag);
|
||||
image.fillInfo()
|
||||
image.on('blobs', processBlobs);
|
||||
const multiArchList = function (manifests) {
|
||||
manifests = manifests.manifests || manifests;
|
||||
self.archs = manifests.map(function (manifest) {
|
||||
return {
|
||||
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ? manifest.platform.variant : ''),
|
||||
digest: manifest.digest
|
||||
}
|
||||
})
|
||||
self.update();
|
||||
};
|
||||
|
||||
this.on('mount', function() {
|
||||
self.refs['tag-history-tag'].tags['material-button'].root.onclick = function() {
|
||||
self.tabchanged = function (arch, idx) {
|
||||
self.elements = []
|
||||
self.image.variants[idx] = self.image.variants[idx] || new registryUI.DockerImage(registryUI.taghistory.image, arch.digest);
|
||||
if (self.image.variants[idx].blobs) {
|
||||
return processBlobs(self.image.variants[idx].blobs);
|
||||
}
|
||||
self.image.variants[idx].fillInfo();
|
||||
self.image.variants[idx].on('blobs', processBlobs);
|
||||
};
|
||||
|
||||
registryUI.taghistory.display = function () {
|
||||
self.elements = []
|
||||
self.image = new registryUI.DockerImage(registryUI.taghistory.image, registryUI.taghistory.tag, true);
|
||||
self.image.fillInfo()
|
||||
self.image.on('blobs', processBlobs);
|
||||
self.image.on('list', multiArchList)
|
||||
};
|
||||
|
||||
this.on('mount', function () {
|
||||
self.refs['tag-history-tag'].tags['material-button'].root.onclick = function () {
|
||||
registryUI.taglist.go(registryUI.taghistory.image);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,33 +16,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<taglist>
|
||||
<!-- Begin of tag -->
|
||||
<material-card ref="taglist-tag" class="taglist" multi-delete={ this.multiDelete }>
|
||||
<div class="material-card-title-action">
|
||||
<material-card class="header">
|
||||
<div class="material-card-title-action ">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onclick="registryUI.home();">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
Tags of { registryUI.name() + '/' + registryUI.taglist.name }
|
||||
Tags of { registryUI.taglist.name }
|
||||
<div class="source-hint">
|
||||
Sourced from { registryUI.name() + '/' + registryUI.taglist.name }
|
||||
</div>
|
||||
<div class="item-count">{ registryUI.taglist.tags.length } tags</div>
|
||||
</h2>
|
||||
</div>
|
||||
<div hide="{ registryUI.taglist.loadend }" class="spinner-wrapper">
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
</material-card>
|
||||
<div hide="{ registryUI.taglist.loadend }" class="spinner-wrapper">
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
<pagination pages="{ registryUI.getPageLabels(this.page, registryUI.getNumPages(registryUI.taglist.tags)) }"></pagination>
|
||||
<material-card ref="taglist-tag" class="taglist"
|
||||
multi-delete={ this.multiDelete }
|
||||
tags={ registryUI.getPage(registryUI.taglist.tags, this.page) }
|
||||
show="{ registryUI.taglist.loadend }">
|
||||
<table show="{ registryUI.taglist.loadend }" style="border: none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="material-card-th-left">Repository</th>
|
||||
<th></th>
|
||||
<th>Creation date</th>
|
||||
<th>Size</th>
|
||||
<th class="creation-date">Creation date</th>
|
||||
<th class="image-size">Size</th>
|
||||
<th id="image-content-digest-header" if="{ registryUI.showContentDigest }">Content Digest</th>
|
||||
|
||||
<th
|
||||
id="image-tag-header"
|
||||
class="{ registryUI.taglist.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
|
||||
onclick="registryUI.taglist.reverse();">Tag
|
||||
</th>
|
||||
<th class="show-tag-history">History</th>
|
||||
<th class={ 'remove-tag': true, delete: this.parent.toDelete > 0 } show="{ registryUI.isImageRemoveActivated }">
|
||||
<th class={ 'remove-tag': true, delete: this.parent.toDelete > 0 } if="{ registryUI.isImageRemoveActivated }">
|
||||
<material-checkbox ref="remove-tag-checkbox" class="indeterminate" show={ this.toDelete === 0} title="Toggle multi-delete. Alt+Click to select all tags."></material-checkbox>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete selected images." onclick={ registryUI.taglist.bulkDelete } show={ this.toDelete > 0 }>
|
||||
<i class="material-icons">delete</i>
|
||||
@@ -50,32 +59,60 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr each="{ image in registryUI.taglist.tags }">
|
||||
<td class="material-card-th-left">{ image.name }</td>
|
||||
<td class="copy-to-clipboard">
|
||||
<copy-to-clipboard image={ image }/>
|
||||
</td>
|
||||
<td>
|
||||
<tr each="{ image in this.opts.tags }">
|
||||
<td class="creation-date">
|
||||
<image-date image="{ image }"/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="image-size">
|
||||
<image-size image="{ image }"/>
|
||||
</td>
|
||||
<td if="{ registryUI.showContentDigest }">
|
||||
<image-content-digest image="{ image }"/>
|
||||
<copy-to-clipboard target="digest" image={ image }/>
|
||||
</td>
|
||||
<td>
|
||||
<image-tag image="{ image }"/>
|
||||
<copy-to-clipboard target="tag" image={ image }/>
|
||||
</td>
|
||||
<td class="show-tag-history">
|
||||
<tag-history-button image={ image }/>
|
||||
</td>
|
||||
<td show="{ registryUI.isImageRemoveActivated }">
|
||||
<td if="{ registryUI.isImageRemoveActivated }">
|
||||
<remove-image multi-delete={ this.opts.multiDelete } image={ image }/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</material-card>
|
||||
<pagination pages="{ registryUI.getPageLabels(this.page, registryUI.getNumPages(registryUI.taglist.tags)) }"></pagination>
|
||||
<script>
|
||||
var self = registryUI.taglist.instance = this;
|
||||
self.page = registryUI.getPageQueryParam();
|
||||
registryUI.taglist.tags = [];
|
||||
const onResize = function() {
|
||||
// window.innerWidth is a blocking access, cache its result.
|
||||
const innerWidth = window.innerWidth;
|
||||
var chars = 0;
|
||||
var max = registryUI.taglist.tags.reduce(function(acc, e) {
|
||||
return e.tag.length > acc ? e.tag.length : acc;
|
||||
}, 0);
|
||||
if (innerWidth >= 1440) {
|
||||
chars = 71;
|
||||
} else if (innerWidth < 1024) {
|
||||
chars = 0;
|
||||
} else {
|
||||
// SHA256:12345678 + scaled between 1024 and 1440px
|
||||
chars = 15 + 56 * ((innerWidth - 1024) / 416);
|
||||
}
|
||||
if (max > 20) chars -= (max - 20);
|
||||
registryUI.taglist.tags.map(function (image) {
|
||||
image.trigger('content-digest-chars', chars);
|
||||
});
|
||||
};
|
||||
window.addEventListener('resize', onResize);
|
||||
// this may be run before the final document size is available, so schedule
|
||||
// a correction once everything is set up.
|
||||
window.requestAnimationFrame(onResize);
|
||||
|
||||
this.multiDelete = false;
|
||||
this.toDelete = 0;
|
||||
@@ -105,6 +142,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
});
|
||||
|
||||
this.on('page-update', function(page) {
|
||||
self.page = page < 1 ? 1 : page;
|
||||
registryUI.updateQueryString(registryUI.getQueryParams({ page: self.page }) );
|
||||
this.toDelete = 0;
|
||||
this.update();
|
||||
});
|
||||
|
||||
this._getRemoveImageTags = function() {
|
||||
var images = self.refs['taglist-tag'].tags['remove-image'];
|
||||
if (!(images instanceof Array)) {
|
||||
@@ -113,8 +157,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return images;
|
||||
};
|
||||
|
||||
registryUI.taglist.bulkDelete = function() {
|
||||
registryUI.taglist.bulkDelete = function(e) {
|
||||
if (self.multiDelete && self.toDelete > 0) {
|
||||
if (e.altKey) {
|
||||
self._getRemoveImageTags()
|
||||
.filter(function(img) { return img.tags['material-checkbox'].checked; })
|
||||
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
|
||||
}
|
||||
self._getRemoveImageTags().filter(function(img) {
|
||||
return img.tags['material-checkbox'].checked;
|
||||
}).forEach(function(img) {
|
||||
@@ -123,19 +172,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
};
|
||||
|
||||
this.on('mount', function() {
|
||||
var toggle = this.tags['material-card'].refs['remove-tag-checkbox'].toggle;
|
||||
this.tags['material-card'].refs['remove-tag-checkbox'].toggle = function(e) {
|
||||
this.on('update', function() {
|
||||
var checkbox = this.refs['taglist-tag'].refs['remove-tag-checkbox'];
|
||||
if (!checkbox || checkbox._toggle) { return; }
|
||||
|
||||
checkbox._toggle = checkbox.toggle;
|
||||
checkbox.toggle = function(e) {
|
||||
if (e.altKey) {
|
||||
if (!this.checked) { this._toggle(); }
|
||||
self._getRemoveImageTags()
|
||||
.filter(function(img) { return !img.tags['material-checkbox'].checked; })
|
||||
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
|
||||
} else {
|
||||
toggle();
|
||||
this._toggle();
|
||||
}
|
||||
};
|
||||
|
||||
this.tags['material-card'].refs['remove-tag-checkbox'].on('toggle', function() {
|
||||
checkbox.on('toggle', function() {
|
||||
registryUI.taglist.instance.multiDelete = this.checked;
|
||||
registryUI.taglist.instance.update();
|
||||
});
|
||||
@@ -149,10 +202,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
oReq.addEventListener('load', function() {
|
||||
registryUI.taglist.tags = [];
|
||||
if (this.status == 200) {
|
||||
registryUI.taglist.tags = JSON.parse(this.responseText).tags || [];
|
||||
registryUI.taglist.tags = registryUI.taglist.tags.map(function(tag) {
|
||||
const tags = JSON.parse(this.responseText).tags || [];
|
||||
registryUI.taglist.tags = tags.map(function(tag) {
|
||||
return new registryUI.DockerImage(registryUI.taglist.name, tag);
|
||||
}).sort(registryUI.DockerImage.compare);
|
||||
window.requestAnimationFrame(onResize);
|
||||
self.trigger('page-update', Math.min(self.page, registryUI.getNumPages(registryUI.taglist.tags)))
|
||||
} else if (this.status == 404) {
|
||||
registryUI.snackbar('Server not found', true);
|
||||
} else {
|
||||
|
||||
@@ -30,9 +30,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /usr/app/dist/ /usr/share/nginx/html/
|
||||
COPY --from=builder /usr/app/dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
|
||||
COPY bin/entrypoint /bin
|
||||
|
||||
ENTRYPOINT entrypoint
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
|
||||
Reference in New Issue
Block a user