Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2590115a7 | ||
|
|
e2f2850f41 | ||
|
|
a8227d9cba | ||
|
|
6a8d984315 | ||
|
|
564bc4b0b4 | ||
|
|
5db01a9fbb | ||
|
|
a9fd1a2a23 | ||
|
|
8b377aee79 | ||
|
|
8c3189b57f | ||
|
|
2dce587840 | ||
|
|
dc64f72483 | ||
|
|
959af86333 | ||
|
|
6314e8b11e | ||
|
|
b9a157c943 | ||
|
|
c3fa2c91d7 | ||
|
|
b3278511fb | ||
|
|
347e201f79 | ||
|
|
c9b2415d1e | ||
|
|
0a6d08bfdd | ||
|
|
1bfbcbf59f | ||
|
|
e4369f4ec9 | ||
|
|
8c402442c2 | ||
|
|
ef240ccf1d | ||
|
|
3693662d98 | ||
|
|
89ae4f64c8 | ||
|
|
2feff56619 | ||
|
|
441def4855 | ||
|
|
9b5b935637 | ||
|
|
367ca0380c | ||
|
|
5983935f84 | ||
|
|
a0dcc84ca6 | ||
|
|
1e08561b3a | ||
|
|
228762c641 | ||
|
|
696aa39012 | ||
|
|
d13f81c9af | ||
|
|
ba2107f765 | ||
|
|
f779f43173 | ||
|
|
dd251b55a0 | ||
|
|
fe724c4d1f | ||
|
|
68d19991ef | ||
|
|
54a954f8c3 | ||
|
|
c7368a3104 | ||
|
|
d11fd42418 | ||
|
|
017f6620f0 | ||
|
|
ee93d5bba8 | ||
|
|
ee1f173355 | ||
|
|
71df95524d | ||
|
|
19e96ab94c | ||
|
|
4e5b768833 | ||
|
|
c1f6c43e4a | ||
|
|
fb8185907e | ||
|
|
636cb60ca8 | ||
|
|
34fd13d6b7 | ||
|
|
57a1cf919a | ||
|
|
ad7c2698fd | ||
|
|
7032ec9f0f | ||
|
|
3b9a804289 | ||
|
|
49ec1d9938 | ||
|
|
c84c9f36e6 | ||
|
|
1939b47677 | ||
|
|
736d527cc8 | ||
|
|
c310845c18 | ||
|
|
f0c7232843 | ||
|
|
49fcba3f6c | ||
|
|
ab12cceefc | ||
|
|
3af4438815 | ||
|
|
f826381681 | ||
|
|
ba2e0b119e | ||
|
|
772d19c18f | ||
|
|
ba6d817b41 | ||
|
|
19e72e4a5f | ||
|
|
05cbb51125 | ||
|
|
7c0874694a | ||
|
|
126509d7fa | ||
|
|
e1fd515279 | ||
|
|
befbd0bcfb | ||
|
|
8ddfb1b5ae | ||
|
|
3a385fc08d | ||
|
|
f958365336 | ||
|
|
29c17b1baa | ||
|
|
b323dc6c04 | ||
|
|
992328eae9 | ||
|
|
dd26bf66a2 | ||
|
|
8fcae3cda4 | ||
|
|
f4455703ca | ||
|
|
22e3f2254e | ||
|
|
4075e0005c | ||
|
|
7c00b85183 | ||
|
|
21e3ad51af | ||
|
|
73613a3b96 | ||
|
|
6da744a9c5 |
2
.github/workflows/main.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build and pus master/main docker images
|
||||
name: Build and push master/main docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
18
.github/workflows/release.yml
vendored
@@ -15,9 +15,15 @@ jobs:
|
||||
run: npm install
|
||||
- name: Build the interface
|
||||
run: npm run build
|
||||
- name: Current tag
|
||||
id: current-tag
|
||||
- name: Major tag
|
||||
id: major-tag
|
||||
run: echo "::set-output name=tag::$(git describe --tags | grep -o '^[0-9]*')"
|
||||
- name: Minor tag
|
||||
id: minor-tag
|
||||
run: echo "::set-output name=tag::$(git describe --tags | grep -o '^[0-9]*\.[0-9]*')"
|
||||
- name: Patch tag
|
||||
id: patch-tag
|
||||
run: echo "::set-output name=tag::$(git describe --tags | grep -o '^[0-9]*\.[0-9]*\.[0-9]*')"
|
||||
- name: Download kokai
|
||||
run: curl -sSL https://github.com/Joxit/kokai/releases/download/$(curl -sSL https://api.github.com/repos/Joxit/kokai/releases/latest | jq -r ".tag_name")/kokai-linux-x86_64 > kokai
|
||||
- name: Create Release Note
|
||||
@@ -51,7 +57,9 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
joxit/docker-registry-ui:latest
|
||||
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}
|
||||
joxit/docker-registry-ui:${{steps.major-tag.outputs.tag}}
|
||||
joxit/docker-registry-ui:${{steps.minor-tag.outputs.tag}}
|
||||
joxit/docker-registry-ui:${{steps.patch-tag.outputs.tag}}
|
||||
- name: Build and push Latest Debian Version
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
@@ -61,4 +69,6 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
joxit/docker-registry-ui:debian
|
||||
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}-debian
|
||||
joxit/docker-registry-ui:${{steps.major-tag.outputs.tag}}-debian
|
||||
joxit/docker-registry-ui:${{steps.minor-tag.outputs.tag}}-debian
|
||||
joxit/docker-registry-ui:${{steps.patch-tag.outputs.tag}}-debian
|
||||
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"quoteProps": "preserve",
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
51
CONTRIBUTING.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# How to contribute to Docke Registry UI
|
||||
|
||||
I ([Jones Magloire](https://joxit.dev/)) created the Docker Registry UI from scratch, but I can't succeed without contributions from community members like you! Contributions come in many different shapes and sizes. In this file we provide guidance around two of the most common types of contributions: opening issues and opening pull requests.
|
||||
|
||||
Please read also the [Code Of Conduct](https://github.com/Joxit/docker-registry-ui/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Submitting Issues
|
||||
|
||||
### Did you find a bug?
|
||||
|
||||
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/joxit/docker-registry-ui/issues).
|
||||
|
||||
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/joxit/docker-registry-ui/issues/new). Be sure to
|
||||
* Use the [**Bug Report Template**](https://github.com/Joxit/docker-registry-ui/issues/new?assignees=&labels=&template=bug_report.md&title=)
|
||||
* Include a **title and clear description**
|
||||
* Write as much relevant information as possible
|
||||
* Add your **full configuration** (Docker Registry UI **AND** Docker Server) or a **screenshots** demonstrating the expected behavior that is not occurring
|
||||
|
||||
## Submitting Pull Request
|
||||
|
||||
### Do you intend to add a new feature or change an existing one?
|
||||
|
||||
* Suggest your change in a new issue using the [**Feature Request Template**](https://github.com/Joxit/docker-registry-ui/issues/new?assignees=&labels=&template=feature_request.md&title=) and start writing code.
|
||||
|
||||
* Run the interface on your computer first with `npm start`.
|
||||
|
||||
### Did you write a patch that fixes a bug?
|
||||
|
||||
* Open a new [GitHub pull request](https://github.com/Joxit/docker-registry-ui/compare) with the patch.
|
||||
|
||||
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
|
||||
|
||||
* Before submitting, please read the [Coding conventions](#coding-conventions) first.
|
||||
|
||||
### Did you fix whitespace, format code, or make a purely cosmetic patch?
|
||||
|
||||
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Docker Registry UI will generally not be accepted.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
* I use prettier with custom preset, use `npm format` before any PR
|
||||
|
||||
* I use [semver](https://semver.org/) for package versioning
|
||||
|
||||
* I use Github Actions for publishing docker images and releases
|
||||
|
||||
* I indent with two spaces
|
||||
|
||||
Thank you for your help! :heart:
|
||||
|
||||
[Joxit](https://joxit.dev/)
|
||||
@@ -34,4 +34,13 @@
|
||||
- Sepp Zuther [@Herr-Sepp](https://github.com/Herr-Sepp)
|
||||
- Tomas Hulata [@tombokombo](https://github.com/tombokombo)
|
||||
- Ben Jackson [@bjj](https://github.com/bjj)
|
||||
- 三十文 [@xfduan](https://github.com/xfduan)
|
||||
- 三十文 [@xfduan](https://github.com/xfduan)
|
||||
- Aram Akhavan [@kaysond](https://github.com/kaysond)
|
||||
- Jason Tackaberry [@jtackaberry](https://github.com/jtackaberry)
|
||||
- Maxime Loliée [@loliee](https://github.com/loliee)
|
||||
- Enrico [@Enrico204](https://github.com/Enrico204)
|
||||
- [@clyvari](https://github.com/clyvari)
|
||||
- Laszlo Boros [@Semmu](https://github.com/Semmu)
|
||||
- [@JKDingwall](https://github.com/JKDingwall)
|
||||
- Martin Herren [@MartinHerren](https://github.com/MartinHerren)
|
||||
- John Daktylidis [@Greek64](https://github.com/Greek64)
|
||||
|
||||
@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
ENV NGINX_LISTEN_PORT '80'
|
||||
ENV SHOW_CATALOG_NB_TAGS 'false'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
|
||||
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx
|
||||
118
README.md
@@ -6,6 +6,8 @@ title: Docker Registry User Interface
|
||||
|
||||

|
||||

|
||||
[](https://github.com/sponsors/Joxit)
|
||||
[](https://artifacthub.io/packages/search?repo=joxit)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -15,35 +17,29 @@ You may need the [migration guide from 1.x to 2.x](https://github.com/Joxit/dock
|
||||
|
||||
This web user interface uses [Riot](https://github.com/Riot/riot) the react-like user interface micro-library and [riot-mui](https://github.com/kysonic/riot-mui) components.
|
||||
|
||||
## [Project Page](https://joxit.dev/docker-registry-ui), [Live Demo](https://joxit.dev/docker-registry-ui/demo/), [Examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples)
|
||||
## [Project Page](https://joxit.dev/docker-registry-ui), [Live Demo](https://joxit.dev/docker-registry-ui/demo/), [Examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples), [Helm Chart](https://helm.joxit.dev/)
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
If you like my work and want to support it, don't hesitate to [sponsor me](https://github.com/sponsors/Joxit).
|
||||
|
||||
- List all your repositories/images.
|
||||
- List all tags for a image.
|
||||
- Sort the tag list with number compatibility (see [#46](https://github.com/Joxit/docker-registry-ui/pull/46)).
|
||||
- Use a secured docker registry.
|
||||
- Display image size (see [#30](https://github.com/Joxit/docker-registry-ui/issues/30)).
|
||||
- Multi arch supports, Alpine and Debian based images with supports for arm32v7 and arm64v8.
|
||||
- Copy `docker pull` command to clipboard (see [#42](https://github.com/Joxit/docker-registry-ui/issues/42)).
|
||||
- Show sha256 for specific tag (hover image tag).
|
||||
- Display image creation date (see [#49](https://github.com/Joxit/docker-registry-ui/issues/49))
|
||||
- Display image history (see [#58](https://github.com/Joxit/docker-registry-ui/pull/58) & [#61](https://github.com/Joxit/docker-registry-ui/pull/61)).
|
||||
- Image aggregation (see [#56](https://github.com/Joxit/docker-registry-ui/issues/56)).
|
||||
- Display image/tag count (see [#56 issue comment](https://github.com/Joxit/docker-registry-ui/issues/56#issuecomment-449246524)).
|
||||
- Select multiple tags to delete (see [#29](https://github.com/Joxit/docker-registry-ui/issues/29)).
|
||||
- Select all tags with ALT + Click to delete (see [#80](https://github.com/Joxit/docker-registry-ui/issues/80)).
|
||||
- One interface for many registries (when `SINGLE_REGISTRY=false`).
|
||||
- Share your docker registry with query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`) (when `SINGLE_REGISTRY=false`).
|
||||
- Use the UI as reverse proxy (with `NGINX_PROXY_PASS_URL` environment variable) to your docker registry (This will avoid CORS).
|
||||
- Add Title when using `REGISTRY_TITLE` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)).
|
||||
- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)).
|
||||
- Add custom header via environment variable and file via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)).
|
||||
- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)).
|
||||
- Multi arch support in history page (see [#130](https://github.com/Joxit/docker-registry-ui/issues/130) and [#134](https://github.com/Joxit/docker-registry-ui/pull/134))
|
||||
## Hidden Features
|
||||
|
||||
- Many ways to delete multiple images at once
|
||||
- Select multiple tags to delete with checkboxes (see [#29](https://github.com/Joxit/docker-registry-ui/issues/29) and [#79](https://github.com/Joxit/docker-registry-ui/pull/79)). Since 1.2.0
|
||||
- Select all tags of the page with `ALT + Click` on the indeterminate checkbox (see [#80](https://github.com/Joxit/docker-registry-ui/issues/80) and [#81](https://github.com/Joxit/docker-registry-ui/pull/81)). Since 1.2.1
|
||||
- Select all contigous tags between two tags with `Shift + Click` on the first tag then `Shift + Click` on the second tag (see [#287](https://github.com/Joxit/docker-registry-ui/pull/287)). Since 2.4.0
|
||||
- Show sha256 for specific tag (hover image tag).
|
||||
- Sort the tag list with number compatibility (see [#45](https://github.com/Joxit/docker-registry-ui/pull/45) and [#46](https://github.com/Joxit/docker-registry-ui/pull/46)). Since 0.4.0
|
||||
- Share your docker registry UI without installation or when you are deploying a UI with `SINGLE_REGISTRY=false`.
|
||||
- Use the public demo and the query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`). If you need credentials on your private registry, you must set the `Access-Control-Allow-Origin` to `https://joxit.dev`.
|
||||
- You can use a single interface with many registry, add them in the menu in the top right of the page.
|
||||
- Filter images and tags with the search bar.
|
||||
- You can select the search bar with the shortcut `CRTL + F` or `F3`. When the search bar is already focused, the shortcut will fallback to the default behavior (see [#213](https://github.com/Joxit/docker-registry-ui/issues/213)). Since 2.1.0
|
||||
- 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)). Since 1.5.0
|
||||
- Show the content of the dockerfile (see [#286](https://github.com/Joxit/docker-registry-ui/pull/286)). Since 2.4.0
|
||||
|
||||
Checkout all options in [Available options](#available-options) section.
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -63,34 +59,67 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- This means you are using a UI with HTTPS and your registry is using HTTP (unsecured). When you are on a HTTPS site, you can't get HTTP content. Upgrade you registry with a HTTPS connection.
|
||||
- Why the default nginx `Host` is set to `$http_host` ?
|
||||
- This fixes the issue [#88](https://github.com/Joxit/docker-registry-ui/issues/88). More about this in [#113](https://github.com/Joxit/docker-registry-ui/issues/113).
|
||||
- Why DELETE fails with 401 status code (using Basic Auth) ?
|
||||
- This is caused by a bug in docker registry, I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104)).
|
||||
- Why OPTIONS (aka preflight requests) and DELETE fails with 401 status code (using Basic Auth) ?
|
||||
- This is caused by a bug in docker registry, it returns 401 status requests on preflight requests, this breaks [W3C preflight-request specification](https://www.w3.org/TR/cors/#preflight-request). I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/ **or** use `NGINX_PROXY_PASS_URL` **or** configure a nginx/apache/haproxy in front of your registry that returns 200 on each OPTIONS requests. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104), [#204](https://github.com/Joxit/docker-registry-ui/issues/204), [#207](https://github.com/Joxit/docker-registry-ui/issues/207), [#214](https://github.com/Joxit/docker-registry-ui/issues/214), [#266](https://github.com/Joxit/docker-registry-ui/issues/266)).
|
||||
- Can I use the docker registry ui as a standalone application (with Electron) ?
|
||||
- Yes, check out the example [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron). (see [#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- I deleted images through the UI, but they are still present on the server. How can I delete them?
|
||||
- When you delete an image with the UI, only the reference is deleted and not the content. To remove dangling images, you need to run the garbage collector of the registry with the command `registry garbage-collect config.yml` or `docker exec registry registry garbage-collect config.yml`. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77) [#147](https://github.com/Joxit/docker-registry-ui/issues/147))
|
||||
- Why when I delete one tag, all tags with the same SHA are deleted ?
|
||||
- This a docker registry API limitation, there is only one way to [delete images with tag](https://docs.docker.com/registry/spec/api/#deleting-an-image), it's by its `name` and its `manifest` (it's a sha of the content). So when you delete a tag, this will delete all tags of this image with the same SHA/manifest.
|
||||
|
||||
- Can I run the container with an unprivileged user ?
|
||||
- Yes you can run the container with the `nginx` user with the option `--user nginx`, this will also update the listen port to `8080` (see [#224](https://github.com/Joxit/docker-registry-ui/issues/224) and [#234](https://github.com/Joxit/docker-registry-ui/pull/234)).
|
||||
- Can I use the UI with a docker hub mirror and show `library/*` images ?
|
||||
- Yes but it is at your own risk using two regstry servers, check the comment [#155](https://github.com/Joxit/docker-registry-ui/issues/155#issuecomment-1286052124).
|
||||
- How to fix CORS issue on s3 bucket ?
|
||||
- You should add a CORS Policy on your bucket, check the issue [#193](https://github.com/Joxit/docker-registry-ui/issues/193).
|
||||
|
||||
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples) or open an issue.
|
||||
|
||||
## Available options
|
||||
|
||||
Some env options are available for use this interface for **only one server**.
|
||||
You can run the container with the unprivileged user `nginx`, see the discussion [#224](https://github.com/Joxit/docker-registry-ui/issues/224).
|
||||
|
||||
- `REGISTRY_URL`: The default url of your docker registry. You may need CORS configuration on your registry. This is usually the domain name or IP of your registry reachable by your computer (e.g `http://registry.example.com`). (default: derived from the hostname of your UI).
|
||||
- `REGISTRY_TITLE`: Set a custom title for your user interface. (default: value derived from `REGISTRY_URL`).
|
||||
- `PULL_URL`: Set a custom url when you copy the `docker pull` command. (default: value derived from `REGISTRY_URL`).
|
||||
Some env options are available for use this interface for **only one server** (when `SINGLE_REGISTRY=true`).
|
||||
|
||||
- `REGISTRY_URL`: The default url of your docker registry. You **may need CORS configuration** on your registry. This is usually the domain name or IP of your registry reachable by your computer (e.g `http://registry.example.com`). (default: derived from the hostname of your UI).
|
||||
- `REGISTRY_TITLE`: Set a custom title for your user interface. (default: value derived from `REGISTRY_URL`) (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28) and [#32](https://github.com/Joxit/docker-registry-ui/issues/32)). Since 0.3.4
|
||||
- `PULL_URL`: Set a custom url when you copy the `docker pull` command (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)). (default: value derived from `REGISTRY_URL`). Since 1.1.0
|
||||
- `DELETE_IMAGES`: Set if we can delete images from the UI. (default: `false`)
|
||||
- `SHOW_CONTENT_DIGEST`: Show content digest in docker tag list. (default: `true`)
|
||||
- `CATALOG_ELEMENTS_LIMIT`: Limit the number of elements in the catalog page. (default: `100000`).
|
||||
- `SINGLE_REGISTRY`: Remove the menu that show the dialogs to add, remove and change the endpoint of your docker registry. (default `false`)
|
||||
- `NGINX_PROXY_PASS_URL`: Update the default Nginx configuration and set the **proxy_pass** to your backend docker registry (this avoid CORS configuration). This is usually the name of your registry container in the form `http://registry:5000`.
|
||||
- `NGINX_PROXY_HEADER_*`: Update the default Nginx configuration and set **custom headers** for your backend docker registry. Only when `NGINX_PROXY_PASS_URL` is used.
|
||||
- `SHOW_CONTENT_DIGEST`: Show/Hide content digest in docker tag list (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126) and [#131](https://github.com/Joxit/docker-registry-ui/pull/131)). (default: `false`). Since 1.4.9
|
||||
- `CATALOG_ELEMENTS_LIMIT`: Limit the number of elements in the catalog page (see [#39](https://github.com/Joxit/docker-registry-ui/issues/39), [#127](https://github.com/Joxit/docker-registry-ui/pull/127) and [#132](https://github.com/Joxit/docker-registry-ui/pull/132)). (default: `100000`). Since 1.4.9
|
||||
- `SINGLE_REGISTRY`: Remove the menu that show the dialogs to add, remove and change the endpoint of your docker registry. (default: `false`). Since 2.0.0
|
||||
- `NGINX_PROXY_PASS_URL`: Update the default Nginx configuration and set the **proxy_pass** to your backend docker registry (this avoid CORS configuration). This is usually the name of your registry container in the form `http://registry:5000`. Since 2.0.0
|
||||
- `NGINX_PROXY_HEADER_*`: Update the default Nginx configuration and **set custom headers** for your backend docker registry via environment variable and file (`/etc/nginx/.env`). Only when `NGINX_PROXY_PASS_URL` is used (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)). Since 1.2.3
|
||||
- `NGINX_PROXY_PASS_HEADER_*`: Update the default Nginx configuration and **forward custom headers** to your backend docker registry via environment variable and file (`/etc/nginx/.env`). Only when `NGINX_PROXY_PASS_URL` is used (see [#206](https://github.com/Joxit/docker-registry-ui/issues/206)). Since 2.1.0
|
||||
- `NGINX_LISTEN_PORT`: Listen on a port other than 80, you can also change the default user and set to nginx `--user nginx` (see [#224](https://github.com/Joxit/docker-registry-ui/issues/224) and [#234](https://github.com/Joxit/docker-registry-ui/pull/234)). (default: `80` when the user is root, `8080` otherwise). Since 2.2.0
|
||||
- `DEFAULT_REGISTRIES`: List of comma separated registry URLs (e.g `http://registry.example.com,http://registry:5000`), available only when `SINGLE_REGISTRY=false` (see [#219](https://github.com/Joxit/docker-registry-ui/pull/219)). (default: ` `). Since 2.1.0
|
||||
- `READ_ONLY_REGISTRIES`: Desactivate dialog for remove and add new registries, available only when `SINGLE_REGISTRY=false` (see [#219](https://github.com/Joxit/docker-registry-ui/pull/219)). (default: `false`). Since 2.1.0
|
||||
- `SHOW_CATALOG_NB_TAGS`: Show number of tags per images on catalog page. This will produce + nb images requests, not recommended on large registries (see [#161](https://github.com/Joxit/docker-registry-ui/issues/161) and [#239](https://github.com/Joxit/docker-registry-ui/pull/239)). (default: `false`). Since 2.2.0
|
||||
- `HISTORY_CUSTOM_LABELS`: Expose custom labels in history page, custom labels will be processed like maintainer label (see [#160](https://github.com/Joxit/docker-registry-ui/issues/160) and [#240](https://github.com/Joxit/docker-registry-ui/pull/240)). Since 2.2.0
|
||||
- `USE_CONTROL_CACHE_HEADER`: Use `Control-Cache` header and set to `no-store, no-cache`. This will avoid some issues on multi-arch images (see [#260](https://github.com/Joxit/docker-registry-ui/issues/260) and [#265](https://github.com/Joxit/docker-registry-ui/pull/265)). This option requires registry configuration: `Access-Control-Allow-Headers` with `Cache-Control`. (default: `false`). Since 2.3.0
|
||||
- `THEME`: Chose your default theme, could be `dark`, `light` or `auto` (see [#283](https://github.com/Joxit/docker-registry-ui/pull/283)). When auto is selected, you will have a switch to manually change from light to dark and vice-versa (see [#291](https://github.com/Joxit/docker-registry-ui/pull/291)). (default: `auto`). Since 2.4.0
|
||||
- `THEME_*`: See table in [Theme options](#theme-options) section (see [#283](https://github.com/Joxit/docker-registry-ui/pull/283)). Since 2.4.0
|
||||
|
||||
There are some examples with [docker-compose](https://docs.docker.com/compose/) and docker-registry-ui as proxy [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy/) or docker-registry-ui as standalone [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone/).
|
||||
|
||||
### Theme options
|
||||
|
||||
This featureswas added to version 2.4.0. See more about this in [#283](https://github.com/Joxit/docker-registry-ui/pull/283).
|
||||
|
||||
| Environment variable | light theme value | dark theme value |
|
||||
| --- | --- | --- |
|
||||
| `THEME_PRIMARY_TEXT` | `#25313b` | `#8A9EBA` |
|
||||
| `THEME_NEUTRAL_TEXT` | `#777777` | `#36527A` |
|
||||
| `THEME_BACKGROUND` | `#ffffff` | `#22272e` |
|
||||
| `THEME_HOVER_BACKGROUND` | `#eeeeee` | `#30404D` |
|
||||
| `THEME_ACCENT_TEXT` | `#6680a1` | `#5684FF` |
|
||||
| `THEME_HEADER_TEXT` | `#ffffff` | `#ffffff` |
|
||||
| `THEME_HEADER_BACKGROUND` | `#25313b` | `#333A45` |
|
||||
| `THEME_FOOTER_TEXT` | `#ffffff` | `#ffffff` |
|
||||
| `THEME_FOOTER_NEUTRAL_TEXT` | `#999999` | `#999999` |
|
||||
| `THEME_FOOTER_BACKGROUND` | `#555555` | `#555555` |
|
||||
|
||||
## Using CORS
|
||||
|
||||
Your server should be configured to accept CORS.
|
||||
@@ -106,15 +135,17 @@ http:
|
||||
headers:
|
||||
Access-Control-Allow-Origin: ['http://registry.example.com']
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS'] # Optional
|
||||
```
|
||||
|
||||
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)).
|
||||
|
||||
:warning: If you are using credential and still having issues, please read the the line about preflight requests and the bug in docker registry server in the [FAQ](#faq) before posting any issues.
|
||||
|
||||
## Using delete
|
||||
|
||||
For deleting images, you need to activate the delete feature in your registry:
|
||||
For deleting images, you need to activate the delete feature in the UI with `DELETE_IMAGES=true` and in your registry:
|
||||
|
||||
```yml
|
||||
storage:
|
||||
@@ -128,11 +159,10 @@ And you need to add these HEADERS:
|
||||
http:
|
||||
headers:
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
```
|
||||
|
||||
If you are running the **static interface** don't forget the environment variable `DELETE_IMAGES`.
|
||||
|
||||
## Registry example
|
||||
|
||||
Example of docker registry configuration file:
|
||||
@@ -155,7 +185,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://127.0.0.1:8000']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
@@ -166,8 +196,9 @@ auth:
|
||||
```
|
||||
|
||||
## 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.
|
||||
check out the [Electron](examples/electron/README.md) standalone application (not updated).
|
||||
|
||||
## All examples
|
||||
|
||||
@@ -182,3 +213,4 @@ check out the [Electron](examples/electron/README.md) standalone application.
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/main/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
|
||||
- [Use DEFAULT_REGISTRIES and READ_ONLY_REGISTRIES](https://github.com/Joxit/docker-registry-ui/tree/main/examples/pr-219) ([#219](https://github.com/Joxit/docker-registry-ui/issues/219))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
title: Docker Registry User Interface
|
||||
description: The simplest and most complete UI for your private registry!
|
||||
url: https://joxit.dev/docker-registry-ui
|
||||
google_analytics: UA-99119327-1
|
||||
google_analytics: G-T158HYBVZ2
|
||||
remote_theme: joxit/joxit.github.io
|
||||
author: Jones Magloire
|
||||
twitter:
|
||||
@@ -12,4 +12,6 @@ defaults:
|
||||
- scope:
|
||||
path: ""
|
||||
values:
|
||||
image: /screenshot.png
|
||||
image: /screenshot.png
|
||||
plugins:
|
||||
- jekyll-seo-tag
|
||||
@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
ENV NGINX_LISTEN_PORT '80'
|
||||
ENV SHOW_CATALOG_NB_TAGS 'false'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
|
||||
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx
|
||||
|
||||
@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
ENV NGINX_LISTEN_PORT '80'
|
||||
ENV SHOW_CATALOG_NB_TAGS 'false'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
|
||||
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx
|
||||
|
||||
75
bin/90-docker-registry-ui.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
|
||||
sed -i "s~\${REGISTRY_URL}~${REGISTRY_URL}~" index.html
|
||||
sed -i "s~\${REGISTRY_TITLE}~${REGISTRY_TITLE}~" index.html
|
||||
sed -i "s~\${PULL_URL}~${PULL_URL}~" index.html
|
||||
sed -i "s~\${SINGLE_REGISTRY}~${SINGLE_REGISTRY}~" index.html
|
||||
sed -i "s~\${CATALOG_ELEMENTS_LIMIT}~${CATALOG_ELEMENTS_LIMIT}~" index.html
|
||||
sed -i "s~\${SHOW_CONTENT_DIGEST}~${SHOW_CONTENT_DIGEST}~" index.html
|
||||
sed -i "s~\${DEFAULT_REGISTRIES}~${DEFAULT_REGISTRIES}~" index.html
|
||||
sed -i "s~\${READ_ONLY_REGISTRIES}~${READ_ONLY_REGISTRIES}~" index.html
|
||||
sed -i "s~\${SHOW_CATALOG_NB_TAGS}~${SHOW_CATALOG_NB_TAGS}~" index.html
|
||||
sed -i "s~\${HISTORY_CUSTOM_LABELS}~${HISTORY_CUSTOM_LABELS}~" index.html
|
||||
sed -i "s~\${USE_CONTROL_CACHE_HEADER}~${USE_CONTROL_CACHE_HEADER}~" index.html
|
||||
|
||||
grep -o 'THEME[A-Z_]*' index.html | while read e; do
|
||||
sed -i "s~\${$e}~$(printenv $e)~" index.html
|
||||
done
|
||||
|
||||
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
|
||||
sed -i "s/\${DELETE_IMAGES}/false/" index.html
|
||||
else
|
||||
sed -i "s/\${DELETE_IMAGES}/true/" index.html
|
||||
fi
|
||||
|
||||
get_nginx_proxy_headers() {
|
||||
(
|
||||
env &&
|
||||
if [ -f "/etc/nginx/.env" ]; then
|
||||
cat /etc/nginx/.env
|
||||
# Force new line
|
||||
echo ""
|
||||
fi
|
||||
) | while read e; do
|
||||
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_HEADER_')" ]; then
|
||||
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_HEADER_//' | sed 's/_/-/g')
|
||||
value=${e#*=}
|
||||
echo -n "proxy_set_header ${key} \"${value}\"; "
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
get_nginx_proxy_pass_headers() {
|
||||
(
|
||||
env &&
|
||||
if [ -f "/etc/nginx/.env" ]; then
|
||||
cat /etc/nginx/.env
|
||||
# Force new line
|
||||
echo ""
|
||||
fi
|
||||
) | while read e; do
|
||||
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_PASS_HEADER_')" ]; then
|
||||
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_PASS_HEADER_//' | sed 's/_/-/g')
|
||||
echo -n "proxy_pass_header \"${key}\"; "
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if [ -n "${NGINX_PROXY_PASS_URL}" ] ; then
|
||||
sed -i "s,\${NGINX_PROXY_PASS_URL},${NGINX_PROXY_PASS_URL}," /etc/nginx/conf.d/default.conf
|
||||
sed -i "s^\${NGINX_PROXY_HEADERS}^$(get_nginx_proxy_headers)^" /etc/nginx/conf.d/default.conf
|
||||
sed -i "s^\${NGINX_PROXY_PASS_HEADERS}^$(get_nginx_proxy_pass_headers)^" /etc/nginx/conf.d/default.conf
|
||||
sed -i "s,#!,," /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
|
||||
if [ "$(whoami)" != "root" ]; then
|
||||
if [ "$NGINX_LISTEN_PORT" = "80" ]; then
|
||||
NGINX_LISTEN_PORT="8080"
|
||||
fi
|
||||
sed -i "/user nginx;/d" /etc/nginx/nginx.conf
|
||||
sed -i "s,/var/run/nginx.pid,/tmp/nginx.pid," /etc/nginx/nginx.conf
|
||||
fi
|
||||
|
||||
if [ "$NGINX_LISTEN_PORT" != "80" ]; then
|
||||
sed -i "s,listen 80;,listen $NGINX_LISTEN_PORT;," /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," index.html
|
||||
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," index.html
|
||||
sed -i "s,\${PULL_URL},${PULL_URL}," index.html
|
||||
sed -i "s,\${SINGLE_REGISTRY},${SINGLE_REGISTRY}," index.html
|
||||
sed -i "s/\${CATALOG_ELEMENTS_LIMIT}/${CATALOG_ELEMENTS_LIMIT}/" index.html
|
||||
sed -i "s/\${SHOW_CONTENT_DIGEST}/${SHOW_CONTENT_DIGEST}/" index.html
|
||||
|
||||
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
|
||||
sed -i "s/\${DELETE_IMAGES}/false/" index.html
|
||||
else
|
||||
sed -i "s/\${DELETE_IMAGES}/true/" index.html
|
||||
fi
|
||||
|
||||
get_nginx_proxy_headers() {
|
||||
(
|
||||
env &&
|
||||
if [ -f "/etc/nginx/.env" ]; then
|
||||
cat /etc/nginx/.env
|
||||
# Force new line
|
||||
echo ""
|
||||
fi
|
||||
) | while read e; do
|
||||
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_HEADER_')" ]; then
|
||||
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_HEADER_//' | sed 's/_/-/g')
|
||||
value=${e#*=}
|
||||
echo -n "proxy_set_header ${key} \"${value}\"; "
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if [ -n "${NGINX_PROXY_PASS_URL}" ] ; then
|
||||
sed -i "s,\${NGINX_PROXY_PASS_URL},${NGINX_PROXY_PASS_URL}," /etc/nginx/conf.d/default.conf
|
||||
sed -i "s^\${NGINX_PROXY_HEADERS}^$(get_nginx_proxy_headers)^" /etc/nginx/conf.d/default.conf
|
||||
sed -i "s,#!,," /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
@@ -12,10 +12,10 @@ for i in arm64v8 arm32v7 master latest debian main; do
|
||||
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$i
|
||||
done
|
||||
|
||||
for v in 1.1 1.2 1.3 1.4 1.5 2.0 ; do
|
||||
for v in 1.1 1.2 1.3 1.4 1.5 2.0 2 2.0.0 2.1 2.1.0 2.2 2.2.0 2.3 2.3.0 2.4 2.4.0; do
|
||||
for type in "-debian" ""; do
|
||||
docker pull joxit/docker-registry-ui:$v$type
|
||||
docker tag joxit/docker-registry-ui:$v$type 127.0.0.1:5000/joxit/docker-registry-ui:$v$type
|
||||
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$v$type
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
@@ -19,8 +19,12 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
ENV NGINX_LISTEN_PORT '80'
|
||||
ENV SHOW_CATALOG_NB_TAGS 'false'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY bin/90-docker-registry-ui.sh /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
|
||||
RUN chown -R nginx:nginx /etc/nginx/ /usr/share/nginx/html/ /var/cache/nginx
|
||||
|
||||
113
demo/index.html
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2018 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,59 +16,72 @@
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="../dist/docker-registry-ui.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta
|
||||
name="description"
|
||||
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui"
|
||||
/>
|
||||
<link rel="canonical" href="https://joxit.dev/docker-registry-ui/demo/" />
|
||||
<meta property="og:url" content="https://joxit.dev/docker-registry-ui/demo/" />
|
||||
<meta property="og:site_name" content="Live Demo | Docker Registry User Interface" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@Joxit" />
|
||||
<meta name="twitter:creator" content="@Jones Magloire" />
|
||||
<title>Live Demo | Docker Registry User Interface</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../dist/docker-registry-ui.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="description"
|
||||
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
|
||||
<meta property="og:description"
|
||||
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
|
||||
<link rel="canonical" href="https://joxit.dev/docker-registry-ui/demo/" />
|
||||
<meta property="og:url" content="https://joxit.dev/docker-registry-ui/demo/" />
|
||||
<meta property="og:site_name" content="Live Demo | Docker Registry User Interface" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@Joxit" />
|
||||
<meta name="twitter:creator" content="@Jones Magloire" />
|
||||
<title>Live Demo | Docker Registry User Interface</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<docker-registry-ui registry-url="" name="Demo Docker Registry UI" pull-url="" show-content-digest="true"
|
||||
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false">
|
||||
<body>
|
||||
<docker-registry-ui
|
||||
registry-url=""
|
||||
name="Demo Docker Registry UI"
|
||||
pull-url=""
|
||||
show-content-digest="true"
|
||||
is-image-remove-activated="true"
|
||||
catalog-elements-limit="1000"
|
||||
single-registry="false"
|
||||
default-registries="https://joxit.dev/docker-registry-demo"
|
||||
show-catalog-nb-tags="true"
|
||||
theme="auto"
|
||||
/>
|
||||
<script>
|
||||
if (localStorage.getItem('registryServer')) {
|
||||
localStorage.setItem('registryServer',
|
||||
localStorage.getItem('registryServer').replace(
|
||||
'https://raw.githubusercontent.com/Joxit/docker-registry-ui/master/demo',
|
||||
'https://joxit.dev/docker-registry-demo'
|
||||
)
|
||||
)
|
||||
} else {
|
||||
localStorage.setItem('registryServer', JSON.stringify([
|
||||
'https://joxit.dev/docker-registry-demo'
|
||||
]))
|
||||
localStorage.setItem(
|
||||
'registryServer',
|
||||
localStorage
|
||||
.getItem('registryServer')
|
||||
.replace(
|
||||
'https://raw.githubusercontent.com/Joxit/docker-registry-ui/master/demo',
|
||||
'https://joxit.dev/docker-registry-demo'
|
||||
)
|
||||
);
|
||||
}
|
||||
</script>
|
||||
<script src="../dist/docker-registry-ui.js"></script>
|
||||
<script>
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function () {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
<script>
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
(i[r] =
|
||||
i[r] ||
|
||||
function () {
|
||||
(i[r].q = i[r].q || []).push(arguments);
|
||||
}),
|
||||
(i[r].l = 1 * new Date());
|
||||
(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m);
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', 'UA-99119327-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
ga('create', 'G-T158HYBVZ2', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
dist/docker-registry-ui.css
vendored
27
dist/docker-registry-ui.js
vendored
2
dist/images/docker-logo.svg
vendored
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" x="0px" y="0px" viewBox="-5.724 -43.601 730 600" fill="#777"> <path d="m595.942,422.343 c -7.543,0.119 -13.562,6.331 -13.443,13.875 0.119,7.544 6.332,13.562 13.875,13.443 7.495,-0.118 13.494,-6.254 13.445,-13.75 -0.085,-7.578 -6.297,-13.652 -13.875,-13.568 0,0 -10e-4,0 0,0 m 0,24.398 c -5.975,0.272 -11.039,-4.352 -11.311,-10.326 -0.271,-5.976 4.352,-11.04 10.327,-11.312 5.975,-0.271 11.039,4.352 11.311,10.327 0.01,0.19 0.013,0.382 0.011,0.573 0.204,5.723 -4.27,10.527 -9.992,10.731 -0.115,0.005 -0.23,0.007 -0.346,0.007"/> <path d="m599.081,436.342 v -0.185 c 1.512,-0.292 2.65,-1.544 2.8,-3.076 0.057,-1.175 -0.432,-2.311 -1.323,-3.077 -1.445,-0.765 -3.076,-1.106 -4.707,-0.984 -1.743,-0.024 -3.484,0.12 -5.2,0.431 v 13.538 h 3.077 v -5.446 h 1.477 c 1.754,0 2.554,0.646 2.83,2.154 0.184,1.143 0.536,2.252 1.047,3.292 h 3.415 c -0.53,-1.062 -0.873,-2.207 -1.016,-3.385 -0.138,-1.473 -1.088,-2.744 -2.462,-3.292 m -3.723,-0.985 h -1.508 v -3.908 c 0.583,-0.069 1.172,-0.069 1.754,0 1.97,0 2.893,0.831 2.893,2.062 0,1.231 -1.415,2 -3.076,2"/> <path d="M707.494,193.557c-1.938-1.539-20.029-15.199-58.181-15.199c-10.074,0.044-20.127,0.908-30.061,2.584 c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908l-6.738,9.723c-8.438,13.061-14.598,27.459-18.214,42.582 c-6.831,28.891-2.677,56.027,11.999,79.226c-17.722,9.876-46.151,12.307-51.904,12.522H22.367 c-12.294,0.017-22.27,9.952-22.337,22.245c-0.549,41.234,6.437,82.222,20.614,120.946c16.214,42.521,40.336,73.842,71.719,93.01 c35.167,21.537,92.302,33.844,157.067,33.844c29.258,0.092,58.461-2.556,87.226-7.907c39.986-7.342,78.463-21.318,113.839-41.352 c29.149-16.88,55.383-38.354,77.688-63.596c37.29-42.213,59.505-89.226,76.026-131.007c2.215,0,4.431,0,6.584,0 c40.828,0,65.935-16.338,79.78-30.029c9.201-8.732,16.384-19.369,21.045-31.167l2.923-8.553L707.494,193.557z"/> <path d="M65.995,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.476-5.538 c-0.01,0-0.021,0-0.031,0H65.995c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C60.488,226.443,62.953,228.909,65.995,228.909L65.995,228.909"/> <path d="M152.913,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.059,0-5.538,2.479-5.538,5.538v56.181C147.392,226.448,149.866,228.909,152.913,228.909"/> <path d="M241.153,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C235.646,226.443,238.112,228.909,241.153,228.909L241.153,228.909"/> <path d="M328.348,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C322.841,226.443,325.307,228.909,328.348,228.909L328.348,228.909"/> <path d="M152.913,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.046,0-5.521,2.46-5.538,5.507v56.181C147.392,145.597,149.861,148.066,152.913,148.083"/> <path d="M241.153,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C235.646,145.591,238.107,148.066,241.153,148.083"/> <path d="M328.348,148.083h63.073c3.052-0.017,5.521-2.486,5.538-5.538V86.364c-0.017-3.047-2.491-5.507-5.538-5.507h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C322.841,145.591,325.302,148.066,328.348,148.083"/> <path d="M328.348,67.227h63.073c3.047,0,5.521-2.461,5.538-5.507V5.507C396.942,2.46,394.468,0,391.421,0h-63.073 c-3.042,0-5.507,2.465-5.507,5.507l0,0v56.212C322.841,64.761,325.307,67.227,328.348,67.227"/> <path d="M416.312,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.041,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C410.805,226.443,413.271,228.909,416.312,228.909"/> </svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 730 730" fill="var(--neutral-text)"> <path d="M603.672 530.973c-7.543.12-13.562 6.331-13.443 13.875.119 7.544 6.332 13.562 13.875 13.443 7.495-.118 13.494-6.254 13.445-13.75-.085-7.578-6.297-13.652-13.875-13.568 0 0-.001 0 0 0m0 24.398c-5.975.272-11.04-4.352-11.311-10.326-.271-5.976 4.352-11.04 10.327-11.312 5.975-.27 11.039 4.352 11.31 10.327.01.19.014.382.012.573.204 5.723-4.27 10.527-9.992 10.731-.115.005-.23.007-.346.007"/> <path d="M606.81 544.972v-.185a3.476 3.476 0 002.8-3.076 3.815 3.815 0 00-1.322-3.077 8.675 8.675 0 00-4.707-.984 27.066 27.066 0 00-5.2.431v13.538h3.077v-5.446h1.477c1.754 0 2.554.646 2.83 2.154a11.667 11.667 0 001.047 3.292h3.415a10.381 10.381 0 01-1.016-3.385 3.938 3.938 0 00-2.462-3.292m-3.723-.985h-1.508v-3.908a7.457 7.457 0 011.754 0c1.97 0 2.893.831 2.893 2.062 0 1.231-1.415 2-3.076 2m112.135-241.924c-1.938-1.539-20.03-15.199-58.181-15.199a185.562 185.562 0 00-30.061 2.584c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908-6.738 9.723a137.318 137.318 0 00-18.214 42.582c-6.831 28.891-2.677 56.027 11.999 79.226-17.722 9.876-46.151 12.307-51.904 12.522H30.096c-12.293.017-22.27 9.952-22.336 22.245a338.69 338.69 0 0020.614 120.946c16.214 42.521 40.336 73.842 71.719 93.01 35.167 21.537 92.302 33.844 157.067 33.844a468.88 468.88 0 0087.226-7.907 364.649 364.649 0 00113.839-41.352 312.969 312.969 0 0077.688-63.596c37.29-42.213 59.505-89.226 76.026-131.007h6.584c40.828 0 65.935-16.338 79.78-30.029a87.08 87.08 0 0021.045-31.167l2.923-8.553z"/> <path d="M73.725 337.54h63.073a5.507 5.507 0 005.507-5.508V275.85a5.507 5.507 0 00-5.476-5.538H73.725a5.507 5.507 0 00-5.507 5.507v56.212a5.507 5.507 0 005.507 5.508m86.918.001h63.073a5.507 5.507 0 005.507-5.508V275.85a5.507 5.507 0 00-5.477-5.538h-63.104a5.537 5.537 0 00-5.538 5.538v56.181a5.54 5.54 0 005.539 5.508m88.24.001h63.073a5.507 5.507 0 005.507-5.508V275.85a5.507 5.507 0 00-5.477-5.538h-63.104a5.507 5.507 0 00-5.507 5.507v56.212a5.51 5.51 0 005.508 5.508m87.195.001h63.073a5.537 5.537 0 005.538-5.508V275.85a5.537 5.537 0 00-5.538-5.538h-63.073a5.507 5.507 0 00-5.507 5.507v56.212a5.508 5.508 0 005.507 5.508m-175.435-80.826h63.073a5.539 5.539 0 005.507-5.538v-56.18a5.507 5.507 0 00-5.507-5.508h-63.073a5.538 5.538 0 00-5.538 5.507v56.181a5.568 5.568 0 005.538 5.538m88.24 0h63.073a5.539 5.539 0 005.507-5.538v-56.18a5.507 5.507 0 00-5.507-5.508h-63.073a5.507 5.507 0 00-5.507 5.507v56.181a5.539 5.539 0 005.507 5.538m87.195 0h63.073a5.568 5.568 0 005.538-5.538v-56.18a5.537 5.537 0 00-5.538-5.508h-63.073a5.507 5.507 0 00-5.507 5.507v56.181a5.539 5.539 0 005.507 5.538m0-80.856h63.073a5.538 5.538 0 005.538-5.507v-56.213a5.537 5.537 0 00-5.538-5.507h-63.073a5.506 5.506 0 00-5.507 5.507v56.212a5.508 5.508 0 005.507 5.508m87.964 161.683h63.073a5.537 5.537 0 005.538-5.508V275.85a5.537 5.537 0 00-5.538-5.538h-63.073a5.508 5.508 0 00-5.507 5.507v56.212a5.508 5.508 0 005.507 5.508"/> </svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
30
dist/index.html
vendored
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -13,6 +13,28 @@
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link href="docker-registry-ui.css" rel="stylesheet" type="text/css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta property="og:site_name" content="Docker Registry UI" /><meta name="twitter:card" content="summary" /><meta name="twitter:site" content="@Joxit" /><meta name="twitter:creator" content="@Jones Magloire" /><title>Docker Registry UI</title></head><body><docker-registry-ui registry-url="${REGISTRY_URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
|
||||
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
|
||||
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}"></docker-registry-ui><script src="docker-registry-ui.js"></script></body></html>
|
||||
--><!DOCTYPE html><html><head><meta charset="UTF-8" /><link href="docker-registry-ui.css" rel="stylesheet" type="text/css"><meta name="viewport" content="width=device-width, initial-scale=1" /><meta property="og:site_name" content="Docker Registry UI" /><meta name="twitter:card" content="summary" /><meta name="twitter:site" content="@Joxit" /><meta name="twitter:creator" content="@Jones Magloire" /><title>Docker Registry UI</title></head><body><docker-registry-ui
|
||||
registry-url="${REGISTRY_URL}"
|
||||
name="${REGISTRY_TITLE}"
|
||||
pull-url="${PULL_URL}"
|
||||
show-content-digest="${SHOW_CONTENT_DIGEST}"
|
||||
is-image-remove-activated="${DELETE_IMAGES}"
|
||||
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}"
|
||||
single-registry="${SINGLE_REGISTRY}"
|
||||
default-registries="${DEFAULT_REGISTRIES}"
|
||||
read-only-registries="${READ_ONLY_REGISTRIES}"
|
||||
show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"
|
||||
history-custom-labels="${HISTORY_CUSTOM_LABELS}"
|
||||
use-control-cache-header="${USE_CONTROL_CACHE_HEADER}"
|
||||
theme="${THEME}"
|
||||
theme-primary-text="${THEME_PRIMARY_TEXT}"
|
||||
theme-neutral-text="${THEME_NEUTRAL_TEXT}"
|
||||
theme-background="${THEME_BACKGROUND}"
|
||||
theme-hover-background="${THEME_HOVER_BACKGROUND}"
|
||||
theme-accent-text="${THEME_ACCENT_TEXT}"
|
||||
theme-header-text="${THEME_HEADER_TEXT}"
|
||||
theme-header-background="${THEME_HEADER_BACKGROUND}"
|
||||
theme-footer-text="${THEME_FOOTER_TEXT}"
|
||||
theme-footer-neutra-text="${THEME_FOOTER_NEUTRAL_TEXT}"
|
||||
theme-footer-background="${THEME_FOOTER_BACKGROUND}"
|
||||
></docker-registry-ui><script src="docker-registry-ui.js"></script></body></html>
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 3.1 MiB |
@@ -10,4 +10,5 @@
|
||||
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/main/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/main/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
|
||||
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/main/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
|
||||
- [Use DEFAULT_REGISTRIES and READ_ONLY_REGISTRIES](https://github.com/Joxit/docker-registry-ui/tree/main/examples/pr-219) ([#219](https://github.com/Joxit/docker-registry-ui/issues/219))
|
||||
@@ -16,10 +16,14 @@ computer.
|
||||
* After building the web application, navigate to the ```electron``` directory
|
||||
and execute following commands to build the executable:
|
||||
```bash
|
||||
cd electron
|
||||
cd examples/electron
|
||||
npm install
|
||||
npm run dist
|
||||
```
|
||||
* Run the application:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
If you encounter any issues, please check the troubleshooting below.
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
appVersion: "1.2.1"
|
||||
description: The simplest and most complete UI for your private registry
|
||||
name: docker-registry-ui
|
||||
home: https://github.com/Joxit/docker-registry-ui
|
||||
keywords:
|
||||
- docker
|
||||
- registry
|
||||
sources:
|
||||
- https://github.com/Joxit/docker-registry-ui
|
||||
version: 0.1.0
|
||||
@@ -1,97 +1,23 @@
|
||||
# docker-registry-ui
|
||||
|
||||
[docker-registry-ui](https://joxit.dev/docker-registry-ui/) is the simplest and most complete UI for your private registry!
|
||||
:warning: The official helm chart is now located at https://helm.joxit.dev and on GitHub [github.com/Joxit/helm-charts](https://github.com/Joxit/helm-charts).
|
||||
|
||||
## Usage
|
||||
|
||||
## TL;DR;
|
||||
1. Add my Helm repository (named `joxit`)
|
||||
|
||||
```bash
|
||||
$ helm install .
|
||||
```
|
||||
helm repo add joxit https://helm.joxit.dev
|
||||
```
|
||||
|
||||
## Introduction
|
||||
2. Ensure you have access to the Helm chart and you see the latest chart version listed. If you have previously added the Helm repository, run `helm repo update`.
|
||||
|
||||
This chart bootstraps a [docker-registry-ui](https://joxit.dev/docker-registry-ui/) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
|
||||
|
||||
It also may deploy the [docker registry](https://docs.docker.com/registry/) if you havent have one already.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.9+ with Beta APIs enabled
|
||||
- PV provisioner support in the underlying infrastructure
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `my-release`:
|
||||
|
||||
```bash
|
||||
$ helm update --install my-release .
|
||||
```
|
||||
helm search repo joxit/docker-registry-ui
|
||||
```
|
||||
|
||||
The command deploys docker-registry-ui on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.
|
||||
3. Now you're ready to install the Docker Registry UI! To install Docker Registry UI with the default configuration using Helm 3.2 run the following command below. This will deploy the Docker Registry UI on the default namespace.
|
||||
|
||||
> **Tip**: List all releases using `helm list`
|
||||
|
||||
## Uninstalling the Chart
|
||||
|
||||
To uninstall/delete the `my-release` deployment:
|
||||
|
||||
```bash
|
||||
$ helm delete my-release
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following table lists the configurable parameters of the Redmine chart and their default values.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------- |
|
||||
| `ui.title` | Title of the managed repository | `Docker registry UI` |
|
||||
| `ui.delete_images` | Allow to delete image from the front-end | `false` |
|
||||
| `ui.proxy` | The UI service act as a proxy of the registry | `true` |
|
||||
| `ui.replicaCount` | Number of replicas to start | `1` |
|
||||
| `ui.image.registry` | registry to pull the docker-registry-ui image from | `docker.io` |
|
||||
| `ui.image.repository` | docker-registry-ui image name | `joxit/docker-registry-ui` |
|
||||
| `ui.image.tag` | docker-registry-ui image tag (change to latest to have multi registry support) | `static` |
|
||||
| `ui.image.pullPolicy` | docker-registry-ui image pull policy | `Always` |
|
||||
| `ui.probe.liveness` | Ask kubernetes to check the service port for liveness | `true` |
|
||||
| `ui.probe.readyness ` | Ask kubernetes to check the service port for readyness | `true` |
|
||||
| `ui.service.type` | Desired service type | `ClusterIP` |
|
||||
| `ui.service.port` | Service exposed port | `80` |
|
||||
| `ui.ingress.enabled` | Create an ingress for docker-regstry-ui | `false` |
|
||||
| `registry.external` | Use an already available registry | `false` |
|
||||
| `registry.url` | URL of the existing registry | `http://localhost:5000` |
|
||||
| `registry.replicaCount` | Number of replicas to start | `1` |
|
||||
| `registry.image.registry` | registry to pull the docker-registry image from | `docker.io` |
|
||||
| `registry.image.repository` | docker-registry-ui image name | `registry` |
|
||||
| `registry.image.tag` | docker-registry-ui image tag | `2.6.2` |
|
||||
| `registry.image.pullPolicy` | docker-registry-ui image pull policy | `Always` |
|
||||
| `registry.probe.liveness` | Ask kubernetes to check the service port for liveness | `true` |
|
||||
| `registry.probe.readyness ` | Ask kubernetes to check the service port for readyness | `true` |
|
||||
| `registry.persistence.enabled` | Enable persistence using PVC for the registry | `false` |
|
||||
| `registry.persistence.storageClass` | PVC Storage Class | `-` |
|
||||
| `registry.persistence.size` | PVC Storage Request size | `1Gi` |
|
||||
| `registry.service.type` | Desired service type | `ClusterIP` |
|
||||
| `registry.service.port` | Service exposed port | `5000` |
|
||||
| `registry.ingress.enabled` | Create an ingress for the regstry | `false` |
|
||||
|
||||
|
||||
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,
|
||||
|
||||
```bash
|
||||
$ helm upgrade --install my-release \
|
||||
--set registry.external=true \
|
||||
--set registry.url=http://registry.example.com:5000 \
|
||||
.
|
||||
helm upgrade --install docker-registry-ui joxit/docker-registry-ui
|
||||
```
|
||||
|
||||
Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example,
|
||||
|
||||
```bash
|
||||
$ helm upgrade --install my-release -f values.yaml .
|
||||
```
|
||||
|
||||
> **Tip**: You can use the default [values.yaml](values.yaml)
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "docker-registry-ui.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "docker-registry-ui.fullname" -}}
|
||||
{{- if .Values.ui.fullnameOverride -}}
|
||||
{{- .Values.ui.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- printf "%s-ui" .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-ui-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry.fullname" -}}
|
||||
{{- if .Values.registry.fullnameOverride -}}
|
||||
{{- .Values.registry.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- printf "%s-registry" .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-registry-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "docker-registry-ui.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "docker-registry-ui.labels" -}}
|
||||
app: registry-ui
|
||||
chart: {{ include "docker-registry-ui.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry-ui.matchLabels" -}}
|
||||
app: registry-ui
|
||||
release: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry.labels" -}}
|
||||
app: registry
|
||||
chart: {{ include "docker-registry-ui.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry.matchLabels" -}}
|
||||
app: registry
|
||||
release: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry-ui.probes" -}}
|
||||
{{- if and .Values.ui.probe.liveness (eq .Values.ui.probe.liveness true) -}}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
{{- end -}}
|
||||
{{- if and .Values.ui.probe.readiness (eq .Values.ui.probe.readiness true) }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry.probes" -}}
|
||||
{{- if and .Values.registry.probe.liveness (eq .Values.registry.probe.liveness true) -}}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /v2/
|
||||
port: registry
|
||||
{{- end -}}
|
||||
{{- if and .Values.registry.probe.readiness (eq .Values.registry.probe.readiness true) }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /v2/
|
||||
port: registry
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry-ui.url-name" -}}
|
||||
{{- if eq .Values.ui.proxy true -}}
|
||||
REGISTRY_URL
|
||||
{{- else -}}
|
||||
URL
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry-ui.url-value" -}}
|
||||
{{- if eq .Values.registry.external true -}}
|
||||
{{ .Values.registry.url }}
|
||||
{{- else -}}
|
||||
{{- $fullName := include "docker-registry.fullname" . -}}
|
||||
{{ printf "http://%s.%s:%.0f" $fullName .Release.Namespace .Values.registry.service.port }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "docker-registry-ui.pull" -}}
|
||||
{{- if eq .Values.registry.external true -}}
|
||||
{{ .Values.registry.url }}
|
||||
{{- else -}}
|
||||
{{- if eq .Values.ui.proxy true -}}
|
||||
{{- if eq .Values.ui.ingress.enabled true -}}
|
||||
{{- $host := index .Values.ui.ingress.hosts 0 -}}
|
||||
{{ $host.host }}
|
||||
{{- else -}}
|
||||
{{- $fullName := include "docker-registry-ui.fullname" . -}}
|
||||
{{ printf "%s.%s:%.0f" $fullName .Release.Namespace .Values.ui.service.port }}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if eq .Values.registry.ingress.enabled true -}}
|
||||
{{- $host := index .Values.registry.ingress.hosts 0 -}}
|
||||
{{ $host.host }}
|
||||
{{- else -}}
|
||||
{{- $fullName := include "docker-registry.fullname" . -}}
|
||||
{{ printf "%s.%s:%.0f" $fullName .Release.Namespace .Values.registry.service.port }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -1,31 +0,0 @@
|
||||
{{- if eq .Values.registry.external false -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "docker-registry.fullname" . }}
|
||||
labels:
|
||||
{{ include "docker-registry.labels" . | indent 4 }}
|
||||
data:
|
||||
config.yml: |-
|
||||
version: 0.1
|
||||
log:
|
||||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
delete:
|
||||
enabled: true
|
||||
cache:
|
||||
blobdescriptor: inmemory
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
http:
|
||||
addr: :5000
|
||||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['*']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
{{- end -}}
|
||||
@@ -1,62 +0,0 @@
|
||||
{{- if eq .Values.registry.external false -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "docker-registry.fullname" . }}
|
||||
labels:
|
||||
{{ include "docker-registry.labels" . | indent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.registry.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "docker-registry.matchLabels" . | indent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "docker-registry.matchLabels" . | indent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
defaultMode: 420
|
||||
name: {{ include "docker-registry.fullname" . }}
|
||||
- name: data
|
||||
{{- if .Values.registry.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "docker-registry.fullname" . }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: registry
|
||||
image: "{{ .Values.registry.image.registry }}/{{ .Values.registry.image.repository }}:{{ .Values.registry.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.registry.image.pullPolicy }}
|
||||
ports:
|
||||
- name: registry
|
||||
containerPort: 5000
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- mountPath: "/var/lib/registry"
|
||||
name: "data"
|
||||
- mountPath: "/etc/docker/registry"
|
||||
name: "config"
|
||||
{{ include "docker-registry.probes" . | indent 10 }}
|
||||
resources:
|
||||
{{- toYaml .Values.registry.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
@@ -1,34 +0,0 @@
|
||||
{{- if and (eq .Values.registry.external false) (and (eq .Values.ui.proxy false) .Values.registry.ingress.enabled) -}}
|
||||
{{- $fullName := include "docker-registry.fullname" . -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{ include "docker-registry.labels" . | indent 4 }}
|
||||
{{- with .Values.registry.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.registry.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.registry.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.registry.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: registry
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
@@ -1,23 +0,0 @@
|
||||
{{- if and (eq .Values.registry.external false) .Values.registry.persistence.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "docker-registry.labels" . | indent 4 }}
|
||||
name: {{ include "docker-registry.fullname" . }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- range .Values.registry.persistence.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.registry.persistence.size }}
|
||||
{{- if .Values.registry.persistence.storageClass }}
|
||||
{{- if (eq "-" .Values.registry.persistence.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: {{ .Values.registry.persistence.storageClass | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
@@ -1,17 +0,0 @@
|
||||
{{- if eq .Values.registry.external false -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "docker-registry.fullname" . }}
|
||||
labels:
|
||||
{{ include "docker-registry.labels" . | indent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.registry.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.registry.service.port }}
|
||||
targetPort: registry
|
||||
protocol: TCP
|
||||
name: registry
|
||||
selector:
|
||||
{{ include "docker-registry.matchLabels" . | indent 6 }}
|
||||
{{- end -}}
|
||||
@@ -1,52 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "docker-registry-ui.fullname" . }}
|
||||
labels:
|
||||
{{ include "docker-registry-ui.labels" . | indent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.ui.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "docker-registry-ui.matchLabels" . | indent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "docker-registry-ui.matchLabels" . | indent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: registry-ui
|
||||
image: "{{ .Values.ui.image.registry }}/{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.ui.image.pullPolicy }}
|
||||
env:
|
||||
- name: REGISTRY_TITLE
|
||||
value: {{ .Values.ui.title| quote }}
|
||||
- name: DELETE_IMAGES
|
||||
value: {{ .Values.ui.delete_images| quote }}
|
||||
- name: {{ include "docker-registry-ui.url-name" . }}
|
||||
value: {{ include "docker-registry-ui.url-value" . | quote }}
|
||||
- name: PULL_URL
|
||||
value: {{ include "docker-registry-ui.pull" . | quote }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
{{ include "docker-registry-ui.probes" . | indent 10 }}
|
||||
resources:
|
||||
{{- toYaml .Values.ui.resources | nindent 12 }}
|
||||
{{- with .Values.ui.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.ui.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.ui.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,34 +0,0 @@
|
||||
{{- if .Values.ui.ingress.enabled -}}
|
||||
{{- $fullName := include "docker-registry-ui.fullname" . -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{ include "docker-registry-ui.labels" . | indent 4 }}
|
||||
{{- with .Values.ui.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ui.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ui.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ui.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "docker-registry-ui.fullname" . }}
|
||||
labels:
|
||||
{{ include "docker-registry-ui.labels" . | indent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.ui.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.ui.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{ include "docker-registry-ui.matchLabels" . | indent 6 }}
|
||||
@@ -1,129 +0,0 @@
|
||||
# Default values for docker-registry-ui.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
|
||||
ui:
|
||||
# title of the registry
|
||||
title: "Docker registry UI"
|
||||
# allow delete of images
|
||||
delete_images: false
|
||||
# UI behave as a proxy of the registry
|
||||
proxy: true
|
||||
|
||||
replicaCount: 1
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: joxit/docker-registry-ui
|
||||
tag: static
|
||||
pullPolicy: Always
|
||||
probe:
|
||||
liveness: true
|
||||
readiness: true
|
||||
|
||||
resources: {}
|
||||
# If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
fullnameOverride: ""
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: docker-registry-ui.local
|
||||
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
|
||||
registry:
|
||||
external: false
|
||||
# URL of the registry (requiered. Note: this wont work as localhost is inside the container. Only used if the registry is external)
|
||||
url: http://localhost:5000
|
||||
|
||||
replicaCount: 1
|
||||
# Image definition for the registry (Only used if the registry is not external)
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: registry
|
||||
tag: 2.7.1
|
||||
pullPolicy: Always
|
||||
probe:
|
||||
liveness: true
|
||||
readiness: true
|
||||
resources: {}
|
||||
# If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
fullnameOverride: ""
|
||||
|
||||
|
||||
persistence:
|
||||
## If true, use a Persistent Volume Claim, If false, use emptyDir
|
||||
##
|
||||
enabled: false
|
||||
## Persistent Volume Storage Class
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||
## GKE, AWS & OpenStack)
|
||||
##
|
||||
# storageClass: "-"
|
||||
## Persistent Volume Claim annotations
|
||||
##
|
||||
annotations:
|
||||
## Persistent Volume Access Mode
|
||||
##
|
||||
accessModes:
|
||||
# This have to be ReadWriteMany if replicaCount>1
|
||||
- ReadWriteOnce
|
||||
## Persistent Volume size
|
||||
##
|
||||
size: 1Gi
|
||||
##
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 5000
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: docker-registry.local
|
||||
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
@@ -10,7 +10,7 @@ kubectl apply -f *.yaml
|
||||
Please note that you'll need a PV provisionner to be able to store the uploaded images.
|
||||
|
||||
## Dynamic installation
|
||||
Edit the image tag in the ui-deployement.yaml file and set it to `latest`, then :
|
||||
Edit the image tag in the ui-deployment.yaml file and set it to `latest`, then :
|
||||
|
||||
```sh
|
||||
kubectl apply -f ui*.yaml
|
||||
|
||||
14
examples/pr-219/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Example for pull request #219
|
||||
|
||||
Basic usage for `DEFAULT_REGISTRIES` and `READ_ONLY_REGISTRIES`.
|
||||
|
||||
Behaviors:
|
||||
- `DEFAULT_REGISTRIES`:
|
||||
- will set the list of registries in the localstorage when the localstorage is empty.
|
||||
- will overwrite the list of registries every time when `READ_ONLY_REGISTRIES=true`
|
||||
- `READ_ONLY_REGISTRIES`:
|
||||
- will remove dialog for Add and Remove registries
|
||||
|
||||
These options works only when `SINGLE_REGISTRY=false`
|
||||
|
||||
See [#219](https://github.com/Joxit/docker-registry-ui/pull/219)
|
||||
39
examples/pr-219/docker-compose.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
registry_1:
|
||||
image: registry:latest
|
||||
restart: always
|
||||
ports:
|
||||
- 5000:5000
|
||||
container_name: registry_1
|
||||
environment:
|
||||
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: "['*']"
|
||||
REGISTRY_STORAGE_DELETE_ENABLED: 'true'
|
||||
volumes:
|
||||
- ./data:/var/lib/registry
|
||||
|
||||
registry_2:
|
||||
image: registry:latest
|
||||
restart: always
|
||||
ports:
|
||||
- 5001:5000
|
||||
container_name: registry_2
|
||||
environment:
|
||||
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: "['*']"
|
||||
REGISTRY_STORAGE_DELETE_ENABLED: 'true'
|
||||
volumes:
|
||||
- ./data:/var/lib/registry
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:latest
|
||||
restart: always
|
||||
container_name: registry-ui
|
||||
environment:
|
||||
- REGISTRY_TITLE=Private Docker Registry
|
||||
- DEFAULT_REGISTRIES=http://localhost:5000,http://localhost:5001
|
||||
- DELETE_IMAGES=true
|
||||
- READ_ONLY_REGISTRIES=true
|
||||
- SINGLE_REGISTRY=false
|
||||
ports:
|
||||
- 80:80
|
||||
@@ -13,6 +13,8 @@ server {
|
||||
# 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;
|
||||
# Fix push and pull of large images: see Issue #282 (https://github.com/Joxit/docker-registry-ui/issues/282)
|
||||
proxy_request_buffering off;
|
||||
proxy_ignore_headers "X-Accel-Buffering";
|
||||
|
||||
location / {
|
||||
@@ -26,7 +28,9 @@ server {
|
||||
#! if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
|
||||
#! return 404;
|
||||
#! }
|
||||
#! proxy_http_version 1.1;
|
||||
#! ${NGINX_PROXY_HEADERS}
|
||||
#! ${NGINX_PROXY_PASS_HEADERS}
|
||||
#! proxy_pass ${NGINX_PROXY_PASS_URL};
|
||||
#! }
|
||||
|
||||
|
||||
50
package.json
@@ -1,8 +1,13 @@
|
||||
{
|
||||
"name": "docker-registry-ui",
|
||||
"version": "2.0.8",
|
||||
"version": "2.4.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "ROLLUP_SERVE=true rollup -c -w",
|
||||
"format": "npm run format-html && npm run format-js && npm run format-riot",
|
||||
"format-html": "find src rollup rollup.config.js -name '*.html' -exec prettier --config .prettierrc -w --parser html {} \\;",
|
||||
"format-js": "find src rollup rollup.config.js -name '*.js' -exec prettier --config .prettierrc -w {} \\;",
|
||||
"format-riot": "find src rollup rollup.config.js -name '*.riot' -exec prettier --config .prettierrc -w --parser html {} \\;",
|
||||
"start": "rollup -c -w --environment ROLLUP_SERVE:true",
|
||||
"build": "rollup -c",
|
||||
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
|
||||
},
|
||||
@@ -14,28 +19,27 @@
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A web UI for private docker registry",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.6",
|
||||
"@babel/preset-env": "^7.14.7",
|
||||
"@riotjs/compiler": "^5.4.2",
|
||||
"@riotjs/observable": "^4.0.4",
|
||||
"@riotjs/route": "^7.1.2",
|
||||
"@rollup/plugin-babel": "^5.2.2",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-html": "^0.2.3",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||
"core-js": "^3.15.2",
|
||||
"js-beautify": "^1.14.0",
|
||||
"riot": "^5.4.5",
|
||||
"riot-mui": "github:joxit/riot-5-mui#4d68d7f",
|
||||
"rollup": "^2.53.1",
|
||||
"@babel/core": "^7.20.7",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@riotjs/compiler": "^6.4.2",
|
||||
"@riotjs/observable": "^4.1.1",
|
||||
"@riotjs/route": "^8.0.2",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-commonjs": "^24.0.0",
|
||||
"@rollup/plugin-html": "^1.0.1",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-terser": "^0.2.1",
|
||||
"core-js": "^3.27.1",
|
||||
"node-sass": "^8.0.0",
|
||||
"prettier": "^2.8.1",
|
||||
"riot": "^7.1.0",
|
||||
"riot-mui": "github:joxit/riot-5-mui#a477acc",
|
||||
"rollup": "^3.9.0",
|
||||
"rollup-plugin-app-utils": "^1.0.6",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-riot": "^5.0.0",
|
||||
"rollup-plugin-scss": "^2.6.1",
|
||||
"rollup-plugin-serve": "^1.1.0",
|
||||
"rollup-plugin-styles": "^3.14.1",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
"rollup-plugin-riot": "^6.0.0",
|
||||
"rollup-plugin-scss": "^4.0.0",
|
||||
"rollup-plugin-serve": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import riot from 'rollup-plugin-riot';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import { emptyDirectories } from 'rollup-plugin-app-utils';
|
||||
import { babel } from '@rollup/plugin-babel';
|
||||
import scss from 'rollup-plugin-scss';
|
||||
import serve from 'rollup-plugin-serve';
|
||||
import html from '@rollup/plugin-html';
|
||||
import htmlUseref from './rollup/html-useref';
|
||||
import htmlUseref from './rollup/html-useref.js';
|
||||
import json from '@rollup/plugin-json';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import copyTransform from './rollup/copy-transform';
|
||||
import license from './rollup/license';
|
||||
import copyTransform from './rollup/copy-transform.js';
|
||||
import license from './rollup/license.js';
|
||||
import checkOutput from './rollup/check-output.js';
|
||||
import importSVG from './rollup/import-svg.js';
|
||||
|
||||
const useServe = process.env.ROLLUP_SERVE === 'true';
|
||||
const output = useServe ? '.serve' : 'dist';
|
||||
@@ -19,9 +21,10 @@ const output = useServe ? '.serve' : 'dist';
|
||||
const plugins = [
|
||||
riot(),
|
||||
json(),
|
||||
importSVG(),
|
||||
nodeResolve(),
|
||||
commonjs(),
|
||||
scss({ output: `./${output}/docker-registry-ui.css`, outputStyle: 'compressed' }),
|
||||
scss({ fileName: `docker-registry-ui.css`, outputStyle: 'compressed' }),
|
||||
babel({ babelHelpers: 'bundled', presets: [['@babel/env', { useBuiltIns: 'usage', corejs: { version: '2' } }]] }),
|
||||
copy({
|
||||
targets: [
|
||||
@@ -44,11 +47,12 @@ export default [
|
||||
dir: output,
|
||||
name: 'DockerRegistryUI',
|
||||
format: 'iife',
|
||||
sourcemap: useServe
|
||||
sourcemap: useServe,
|
||||
},
|
||||
plugins: [emptyDirectories(output)].concat(
|
||||
plugins,
|
||||
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) })
|
||||
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) }),
|
||||
checkOutput(output)
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
40
rollup/check-output.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const files = [
|
||||
'docker-registry-ui.css',
|
||||
'docker-registry-ui.js',
|
||||
'fonts/MaterialIcons-Regular.eot',
|
||||
'fonts/MaterialIcons-Regular.svg',
|
||||
'fonts/MaterialIcons-Regular.ttf',
|
||||
'fonts/MaterialIcons-Regular.woff',
|
||||
'fonts/MaterialIcons-Regular.woff2',
|
||||
'fonts/Roboto-Bold.ttf',
|
||||
'fonts/Roboto-Bold.woff',
|
||||
'fonts/Roboto-Bold.woff2',
|
||||
'fonts/Roboto-Light.ttf',
|
||||
'fonts/Roboto-Light.woff',
|
||||
'fonts/Roboto-Light.woff2',
|
||||
'fonts/RobotoMono-Regular.eot',
|
||||
'fonts/RobotoMono-Regular.ttf',
|
||||
'fonts/RobotoMono-Regular.woff',
|
||||
'fonts/RobotoMono-Regular.woff2',
|
||||
'fonts/Roboto-Regular.eot',
|
||||
'fonts/Roboto-Regular.ttf',
|
||||
'fonts/Roboto-Regular.woff',
|
||||
'fonts/Roboto-Regular.woff2',
|
||||
'images/docker-logo.svg',
|
||||
'index.html',
|
||||
];
|
||||
|
||||
export default function (output) {
|
||||
return {
|
||||
name: 'check-output',
|
||||
writeBundle: () => {
|
||||
const missingFile = files.find((file) => !fs.existsSync(path.join(output, file)));
|
||||
if (missingFile) {
|
||||
throw new Error(`File ${missingFile} is missing after build`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
28
rollup/import-svg.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { extname } from 'path';
|
||||
|
||||
const injectNode = (svg) => `
|
||||
export default function() {
|
||||
return (new DOMParser().parseFromString(${svg}, 'image/svg+xml'));
|
||||
};
|
||||
`;
|
||||
|
||||
/**
|
||||
* @param options
|
||||
* @param options.include
|
||||
* @param options.exclude
|
||||
* @param options.stringify - if true returns String, otherwise returns DOM Node
|
||||
*/
|
||||
export default function () {
|
||||
return {
|
||||
name: 'import-svg',
|
||||
transform: (code, id) => {
|
||||
if (extname(id) !== '.svg') return null;
|
||||
const content = JSON.stringify(code);
|
||||
|
||||
return {
|
||||
code: injectNode(content),
|
||||
map: { mappings: '' },
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export default `/*
|
||||
* Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
* Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,4 +15,4 @@ export default `/*
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @license AGPL
|
||||
*/`
|
||||
*/`;
|
||||
|
||||
BIN
screenshot.png
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 44 KiB |
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,11 +16,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<catalog-element>
|
||||
<!-- Begin of tag -->
|
||||
<div class="content"
|
||||
if="{!props.filterResults || state.nImages > 0 || matchSearch(props.filterResults, state.image)}">
|
||||
<div
|
||||
class="content"
|
||||
if="{!props.filterResults || state.nImages > 0 || matchSearch(props.filterResults, state.image)}"
|
||||
>
|
||||
<material-card class="list highlight" expanded="{state.expanded}" onclick="{ onClick }">
|
||||
<material-waves onmousedown="{this.triggerLaunch}" center="true" color="#ddd"
|
||||
setLaunchListener="{ setLaunchListener }" />
|
||||
<a if="{ state.image }" href="{ router.taglist(state.image) }">
|
||||
<material-waves center="true" color="#ddd"></material-waves>
|
||||
</a>
|
||||
<material-waves if="{ state.images }" center="true" color="#ddd"></material-waves>
|
||||
<span>
|
||||
<i class="material-icons">send</i>
|
||||
{ state.image || state.repo }
|
||||
@@ -28,17 +32,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
{ state.nImages } images
|
||||
<i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i>
|
||||
</div>
|
||||
<div if="{props.showCatalogNbTags && state.image}" class="item-count right">
|
||||
{ state.nbTags } tags
|
||||
<i class="material-icons animated"></i>
|
||||
</div>
|
||||
</span>
|
||||
</material-card>
|
||||
<catalog-element if="{ state.images }" filter-results="{ props.filterResults }"
|
||||
<catalog-element
|
||||
if="{ state.images }"
|
||||
filter-results="{ props.filterResults }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
on-notify="{ props.onnNotify }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
show-catalog-nb-tags="{ props.showCatalogNbTags }"
|
||||
class="animated {!state.expanded && !props.filterResults ? 'hide' : ''} {state.expanding ? 'expanding' : ''}"
|
||||
each="{item in state.images}" item="{ item }" />
|
||||
each="{item in state.images}"
|
||||
item="{ item }"
|
||||
></catalog-element>
|
||||
</div>
|
||||
<script>
|
||||
import router from '../../scripts/router';
|
||||
import {
|
||||
matchSearch
|
||||
} from '../search-bar.riot';
|
||||
import { Http } from '../../scripts/http';
|
||||
import { matchSearch } from '../search-bar.riot';
|
||||
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
@@ -51,35 +66,55 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
state.repo = props.item.repo;
|
||||
state.nImages = props.item.images.length;
|
||||
}
|
||||
if (props.showCatalogNbTags && state.image) {
|
||||
this.getNbTags(props, state);
|
||||
}
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
if (props.filterResults && state.images) {
|
||||
state.nImages = state.images.filter(image => matchSearch(props.filterResults, image)).length;
|
||||
state.nImages = state.images.filter((image) => matchSearch(props.filterResults, image)).length;
|
||||
} else {
|
||||
state.nImages = state.images && state.images.length;
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
const state = this.state;
|
||||
if (!state.repo) {
|
||||
router.taglist(state.image);
|
||||
} else {
|
||||
if (state.repo) {
|
||||
this.update({
|
||||
expanded: !this.state.expanded,
|
||||
expanding: true
|
||||
expanding: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.update({
|
||||
expanding: false
|
||||
expanding: false,
|
||||
});
|
||||
}, 50)
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
setLaunchListener(cb) {
|
||||
this.triggerLaunch = cb;
|
||||
getNbTags(props, state) {
|
||||
const self = this;
|
||||
const oReq = new Http({
|
||||
onAuthentication: props.onAuthentication,
|
||||
});
|
||||
oReq.addEventListener('load', function () {
|
||||
if (this.status === 200) {
|
||||
const nbTags = (JSON.parse(this.responseText).tags || []).length;
|
||||
self.update({ nbTags });
|
||||
} else if (this.status === 404) {
|
||||
props.onNotify('Server not found', true);
|
||||
} else {
|
||||
props.onNotify(this.responseText, true);
|
||||
}
|
||||
});
|
||||
oReq.addEventListener('error', function () {
|
||||
props.onNotify(this.getErrorMessage(), true);
|
||||
});
|
||||
oReq.open('GET', props.registryUrl + '/v2/' + state.image + '/tags/list');
|
||||
oReq.send();
|
||||
},
|
||||
matchSearch
|
||||
}
|
||||
matchSearch,
|
||||
router,
|
||||
};
|
||||
</script>
|
||||
<!-- End of tag -->
|
||||
</catalog-element>
|
||||
</catalog-element>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -26,50 +26,65 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div if="{ !state.loadend }" class="spinner-wrapper">
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
<catalog-element each="{ item in state.repositories }" item="{ item }" filter-results="{ props.filterResults }"/>
|
||||
<catalog-element
|
||||
each="{ item in state.repositories }"
|
||||
item="{ item }"
|
||||
filter-results="{ props.filterResults }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
show-catalog-nb-tags="{ props.showCatalogNbTags }"
|
||||
></catalog-element>
|
||||
<script>
|
||||
import CatalogElement from './catalog-element.riot'
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import CatalogElement from './catalog-element.riot';
|
||||
import { Http } from '../../scripts/http';
|
||||
import { getRegistryServers } from '../../scripts/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CatalogElement
|
||||
CatalogElement,
|
||||
},
|
||||
state: {
|
||||
registryName: '',
|
||||
length: 0,
|
||||
loadend: false,
|
||||
repositories: []
|
||||
repositories: [],
|
||||
registryUrl: '',
|
||||
},
|
||||
|
||||
onBeforeMount(props) {
|
||||
this.state.registryName = props.registryName;
|
||||
this.state.catalogElementsLimit = props.catalogElementsLimit;
|
||||
},
|
||||
onMounted(props) {
|
||||
this.display(props, this.state)
|
||||
onMounted(props, state) {
|
||||
this.display(props, state);
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
this.display(props, state);
|
||||
},
|
||||
|
||||
display(props, state) {
|
||||
if (props.registryUrl === state.registryUrl) {
|
||||
return;
|
||||
}
|
||||
state.registryUrl = props.registryUrl;
|
||||
let repositories = [];
|
||||
const self = this;
|
||||
const catalogUrl = `${props.registryUrl}/v2/_catalog?n=${state.catalogElementsLimit}`;
|
||||
const oReq = new Http({
|
||||
onAuthentication: this.props.onAuthentication
|
||||
onAuthentication: this.props.onAuthentication,
|
||||
});
|
||||
oReq.addEventListener('load', function () {
|
||||
if (this.status == 200) {
|
||||
if (this.status === 200) {
|
||||
repositories = JSON.parse(this.responseText).repositories || [];
|
||||
repositories.sort();
|
||||
repositories = repositories.reduce(function (acc, e) {
|
||||
const slash = e.indexOf('/');
|
||||
if (slash > 0) {
|
||||
const repoName = e.substring(0, slash) + '/';
|
||||
if (acc.length == 0 || acc[acc.length - 1].repo != repoName) {
|
||||
if (acc.length === 0 || acc[acc.length - 1].repo != repoName) {
|
||||
acc.push({
|
||||
repo: repoName,
|
||||
images: []
|
||||
images: [],
|
||||
});
|
||||
}
|
||||
acc[acc.length - 1].images.push(e);
|
||||
@@ -78,8 +93,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
acc.push(e);
|
||||
return acc;
|
||||
}, []);
|
||||
} else if (this.status == 404) {
|
||||
self.props.onNotify('Server not found', true);
|
||||
} else if (this.status === 404) {
|
||||
self.props.onNotify({ code: 'CATALOG_NOT_FOUND', url: catalogUrl }, true);
|
||||
} else {
|
||||
self.props.onNotify(this.responseText);
|
||||
}
|
||||
@@ -91,13 +106,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
self.update({
|
||||
repositories,
|
||||
nRepositories: repositories.length,
|
||||
nImages: repositories.reduce((acc, e) => acc + (e.images && e.images.length || 1), 0),
|
||||
loadend: true
|
||||
nImages: repositories.reduce((acc, e) => acc + ((e.images && e.images.length) || 1), 0),
|
||||
loadend: true,
|
||||
});
|
||||
});
|
||||
oReq.open('GET', `${props.registryUrl}/v2/_catalog?n=${state.catalogElementsLimit}`);
|
||||
oReq.open('GET', catalogUrl);
|
||||
oReq.send();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</catalog>
|
||||
</catalog>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,24 +16,41 @@
|
||||
-->
|
||||
<add-registry-url>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
|
||||
<div slot="title">Add your Server ?</div>
|
||||
<div slot="content">
|
||||
<material-input onkeyup="{ onKeyUp }" placeholder="Server URL"></material-input>
|
||||
<div class="material-popup-title">Add your Server ?</div>
|
||||
<div class="material-popup-content">
|
||||
<material-input
|
||||
onkeyup="{ onKeyUp }"
|
||||
label="Server URL"
|
||||
text-color="var(--primary-text)"
|
||||
label-color="var(--neutral-text)"
|
||||
color="var(--accent-text)"
|
||||
valid="{ registryUrlValidator }"
|
||||
></material-input>
|
||||
<span>Write your URL without /v2</span>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ add }">
|
||||
<div class="material-popup-action">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="var(--hover-background)"
|
||||
onClick="{ add }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Add
|
||||
</material-button>
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="var(--hover-background)"
|
||||
onClick="{ props.onClose }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Cancel
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import { addRegistryServers } from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
|
||||
export default {
|
||||
@@ -51,14 +68,15 @@
|
||||
if (!input.value.startsWith('http')) {
|
||||
return this.props.onNotify('The input field should start with http:// or https://.', true);
|
||||
}
|
||||
const url = input.value.trim().replace(/\/*$/, '');
|
||||
const registryServer = getRegistryServers().filter(e => e !== url);
|
||||
localStorage.setItem('registryServer', JSON.stringify([url].concat(registryServer)));
|
||||
router.home()
|
||||
const url = addRegistryServers(input.value);
|
||||
router.home();
|
||||
this.props.onServerChange(url);
|
||||
this.props.onClose()
|
||||
this.props.onClose();
|
||||
setTimeout(() => router.updateUrlQueryParam(url), 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
registryUrlValidator(input) {
|
||||
return /^https?:\/\//.test(input) && !/\/v2\/?$/.test(input);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</add-registry-url>
|
||||
</add-registry-url>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,26 +15,36 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<change-registry-url>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClick }">
|
||||
<div slot="title">Change your Server ?</div>
|
||||
<div slot="content">
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
|
||||
<div class="material-popup-title">Change your Server ?</div>
|
||||
<div class="material-popup-content">
|
||||
<select>
|
||||
<option each="{ url in getRegistryServers() }" value="{ url }">{ url }</option>
|
||||
</select>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ change }">
|
||||
<div class="material-popup-action">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="var(--hover-background)"
|
||||
onClick="{ change }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Change
|
||||
</material-button>
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="var(--hover-background)"
|
||||
onClick="{ props.onClose }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Cancel
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import { addRegistryServers, getRegistryServers } from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
change(event) {
|
||||
@@ -45,16 +55,14 @@
|
||||
if (!select.value.startsWith('http')) {
|
||||
return this.props.onNotify('The select field should start with http:// or https://.', true);
|
||||
}
|
||||
const url = select.value.trim().replace(/\/*$/, '');
|
||||
const registryServer = getRegistryServers().filter(e => e !== url);
|
||||
localStorage.setItem('registryServer', JSON.stringify([url].concat(registryServer)));
|
||||
router.home()
|
||||
const url = addRegistryServers(select.value);
|
||||
router.home();
|
||||
this.props.onServerChange(url);
|
||||
this.props.onClose()
|
||||
this.props.onClose();
|
||||
setTimeout(() => router.updateUrlQueryParam(url), 100);
|
||||
},
|
||||
getRegistryServers
|
||||
}
|
||||
getRegistryServers,
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host select {
|
||||
@@ -66,13 +74,18 @@
|
||||
background: 0 0;
|
||||
border: none;
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
border-bottom: 1px solid #2f6975;
|
||||
border-bottom: 1px solid var(--accent-text);
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
margin: 1.5em 0;
|
||||
color: var(--primary-text);
|
||||
}
|
||||
:host select option {
|
||||
background-color: var(--background);
|
||||
}
|
||||
</style>
|
||||
</change-registry-url>
|
||||
</change-registry-url>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,82 +16,112 @@
|
||||
-->
|
||||
<confirm-delete-image>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClick }">
|
||||
<div slot="title">These images will be deleted</div>
|
||||
<div slot="content">
|
||||
<div class="material-popup-title">These images will be deleted</div>
|
||||
<div class="material-popup-content">
|
||||
<ul>
|
||||
<li each="{ image in displayImagesToDelete(props.toDelete, props.tags) }">{ image.name }:{ image.tag }</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ deleteImages }">
|
||||
<div class="material-popup-action">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="var(--hover-background)"
|
||||
onClick="{ deleteImages }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Delete
|
||||
</material-button>
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClick }">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="var(--hover-background)"
|
||||
onClick="{ props.onClick }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Cancel
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import { Http } from '../../scripts/http';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
displayImagesToDelete(toDelete, tags) {
|
||||
const digests = new Set();
|
||||
toDelete.forEach(image => {
|
||||
if (image.digest) {
|
||||
digests.add(image.digest);
|
||||
const contentDigests = new Set();
|
||||
toDelete.forEach((image) => {
|
||||
if (image.contentDigest) {
|
||||
contentDigests.add(image.contentDigest);
|
||||
}
|
||||
})
|
||||
return tags.filter(image => digests.has(image.digest))
|
||||
});
|
||||
return tags.filter((image) => contentDigests.has(image.contentDigest));
|
||||
},
|
||||
deleteImages() {
|
||||
this.props.toDelete.forEach(image => this.deleteImage(image, this.props));
|
||||
this.props.toDelete.forEach((image) => this.getContentDigestThenDelete(image, this.props));
|
||||
},
|
||||
deleteImage(image, opts) {
|
||||
const {
|
||||
registryUrl,
|
||||
ignoreError,
|
||||
onNotify,
|
||||
onAuthentication,
|
||||
onClick
|
||||
} = opts;
|
||||
if (!image.digest) {
|
||||
onNotify(`Information for ${name}:${tag} are not yet loaded.`);
|
||||
return;
|
||||
}
|
||||
const name = image.name;
|
||||
const tag = image.tag;
|
||||
const oReq = new Http({
|
||||
onAuthentication: onAuthentication
|
||||
});
|
||||
getContentDigestThenDelete({ name, tag }, opts) {
|
||||
const { registryUrl, onNotify, onAuthentication } = opts;
|
||||
const oReq = new Http({ onAuthentication });
|
||||
const self = this;
|
||||
oReq.addEventListener('loadend', function () {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
if (this.status === 200 || this.status === 202) {
|
||||
oReq.getContentDigest(function (contentDigest) {
|
||||
if (!contentDigest) {
|
||||
onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
|
||||
} else {
|
||||
self.deleteImage({ name, tag, contentDigest }, opts);
|
||||
}
|
||||
});
|
||||
} else if (this.status === 404) {
|
||||
onNotify(`Manifest for ${name}:${tag} not found`, true);
|
||||
} else {
|
||||
onNotify(this.responseText);
|
||||
}
|
||||
});
|
||||
oReq.open('GET', `${registryUrl}/v2/${name}/manifests/${tag}`);
|
||||
oReq.setRequestHeader(
|
||||
'Accept',
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json'
|
||||
);
|
||||
oReq.send();
|
||||
},
|
||||
deleteImage({ name, tag, contentDigest }, opts) {
|
||||
const { registryUrl, ignoreError, onNotify, onAuthentication, onClick } = opts;
|
||||
const oReq = new Http({ onAuthentication });
|
||||
oReq.addEventListener('loadend', function () {
|
||||
if (this.status === 200 || this.status === 202) {
|
||||
router.taglist(name);
|
||||
onNotify(`Deleting ${name}:${tag} image. Run \`registry garbage-collect config.yml\` on your registry`);
|
||||
} else if (this.status == 404) {
|
||||
ignoreError || onNotify({
|
||||
message: 'Digest not found for this image in your registry.',
|
||||
isError: true
|
||||
});
|
||||
} else if (this.status === 404) {
|
||||
ignoreError ||
|
||||
onNotify({
|
||||
message: 'Digest not found for this image in your registry.',
|
||||
isError: true,
|
||||
});
|
||||
} else {
|
||||
onNotify(this.responseText);
|
||||
}
|
||||
onClick();
|
||||
});
|
||||
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${image.digest}`);
|
||||
oReq.setRequestHeader('Accept',
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
||||
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${contentDigest}`);
|
||||
oReq.setRequestHeader(
|
||||
'Accept',
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json'
|
||||
);
|
||||
oReq.addEventListener('error', function () {
|
||||
const credMsg = this.withCredentials
|
||||
? ' When you use credentials on a different hostname, the registry server may fail preflight requests. Check FAQ and issue #104.'
|
||||
: '';
|
||||
onNotify({
|
||||
message: 'An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].',
|
||||
isError: true
|
||||
message:
|
||||
"An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: ['DELETE']." +
|
||||
credMsg,
|
||||
isError: true,
|
||||
});
|
||||
});
|
||||
oReq.send();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host {
|
||||
@@ -105,4 +135,4 @@
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
</confirm-delete-image>
|
||||
</confirm-delete-image>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,18 +15,43 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<dialogs-menu>
|
||||
<add-registry-url opened="{ state['add-registry-url'] }" on-close="{ onClose('add-registry-url') }"
|
||||
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></add-registry-url>
|
||||
<change-registry-url opened="{ state['change-registry-url'] }" on-close="{ onClose('change-registry-url') }"
|
||||
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></change-registry-url>
|
||||
<remove-registry-url opened="{ state['remove-registry-url'] }" on-close="{ onClose('remove-registry-url') }"
|
||||
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></remove-registry-url>
|
||||
<div class="container">
|
||||
<material-button onClick="{ onClick }" waves-center="true" rounded="true" waves-opacity="0.6" waves-duration="600">
|
||||
<add-registry-url
|
||||
if="{ !props.readOnlyRegistries }"
|
||||
opened="{ state['add-registry-url'] }"
|
||||
on-close="{ onClose('add-registry-url') }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-server-change="{ props.onServerChange }"
|
||||
></add-registry-url>
|
||||
<change-registry-url
|
||||
opened="{ state['change-registry-url'] }"
|
||||
on-close="{ onClose('change-registry-url') }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-server-change="{ props.onServerChange }"
|
||||
></change-registry-url>
|
||||
<remove-registry-url
|
||||
if="{ !props.readOnlyRegistries }"
|
||||
opened="{ state['remove-registry-url'] }"
|
||||
on-close="{ onClose('remove-registry-url') }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-server-change="{ props.onServerChange }"
|
||||
></remove-registry-url>
|
||||
<div class="material-dropdown-wrapper">
|
||||
<material-button
|
||||
onClick="{ onClick }"
|
||||
waves-center="true"
|
||||
waves-opacity="0.6"
|
||||
waves-duration="600"
|
||||
color="var(--header-background)"
|
||||
text-color="var(--header-text)"
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">more_vert</i>
|
||||
</material-button>
|
||||
<material-dropdown-list items="{ dropdownItems }" onSelect="{ onDropdownSelect }"
|
||||
opened="{ state.isDropdownOpened }" />
|
||||
<material-dropdown
|
||||
items="{ dropdownItems.filter(item => item.ro || !props.readOnlyRegistries) }"
|
||||
on-click="{ onDropdownSelect }"
|
||||
opened="{ state.isDropdownOpened }"
|
||||
></material-dropdown>
|
||||
</div>
|
||||
<div class="overlay" onclick="{ onClick }" if="{ state.isDropdownOpened }"></div>
|
||||
<script>
|
||||
@@ -38,48 +63,66 @@
|
||||
components: {
|
||||
AddRegistryUrl,
|
||||
ChangeRegistryUrl,
|
||||
RemoveRegistryUrl
|
||||
RemoveRegistryUrl,
|
||||
},
|
||||
dropdownItems: [{
|
||||
title: 'Add URL',
|
||||
name: 'add-registry-url'
|
||||
}, {
|
||||
title: 'Change URL',
|
||||
name: 'change-registry-url'
|
||||
}, {
|
||||
title: 'Remove URL',
|
||||
name: 'remove-registry-url'
|
||||
}],
|
||||
onDropdownSelect(key, item) {
|
||||
dropdownItems: [
|
||||
{
|
||||
title: 'Add URL',
|
||||
name: 'add-registry-url',
|
||||
ro: false,
|
||||
},
|
||||
{
|
||||
title: 'Change URL',
|
||||
name: 'change-registry-url',
|
||||
ro: true,
|
||||
},
|
||||
{
|
||||
title: 'Remove URL',
|
||||
name: 'remove-registry-url',
|
||||
ro: false,
|
||||
},
|
||||
],
|
||||
onDropdownSelect(event) {
|
||||
this.update({
|
||||
[item.name]: true,
|
||||
isDropdownOpened: false
|
||||
[event.target.item]: true,
|
||||
isDropdownOpened: false,
|
||||
});
|
||||
},
|
||||
onClose(name) {
|
||||
return () => {
|
||||
this.update({
|
||||
[name]: false,
|
||||
isDropdownOpened: false
|
||||
})
|
||||
}
|
||||
isDropdownOpened: false,
|
||||
});
|
||||
};
|
||||
},
|
||||
onClick() {
|
||||
this.update({
|
||||
isDropdownOpened: !this.state.isDropdownOpened
|
||||
})
|
||||
}
|
||||
}
|
||||
isDropdownOpened: !this.state.isDropdownOpened,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host > .container{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 16px;
|
||||
color: #000;
|
||||
:host > .material-dropdown-wrapper {
|
||||
color: var(--primary-text);
|
||||
list-style-type: disc;
|
||||
margin-block-start: 0.7em;
|
||||
}
|
||||
material-dropdown .material-dropdown-container,
|
||||
material-dropdown .material-dropdown-container .material-dropdown-item {
|
||||
background-color: var(--background);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
material-dropdown .material-dropdown-container .material-dropdown-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
:host .material-dropdown-wrapper material-dropdown .material-dropdown-container {
|
||||
right: 0;
|
||||
top: 2em;
|
||||
}
|
||||
|
||||
:host .overlay {
|
||||
position: fixed;
|
||||
@@ -90,33 +133,15 @@
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:host material-button {
|
||||
background: rgba(255, 255, 255, 0);
|
||||
:host material-button button {
|
||||
float: right;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
:host material-button .content i.material-icons {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
:host material-dropdown-list {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host material-dropdown-list ul.dropdown-content {
|
||||
min-width: 156px;
|
||||
padding: 8px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:host material-dropdown-list ul.dropdown-content li span {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
:host material-popup * {
|
||||
line-height: 1em;
|
||||
}
|
||||
@@ -125,4 +150,4 @@
|
||||
line-height: 36px;
|
||||
}
|
||||
</style>
|
||||
</dialogs-menu>
|
||||
</dialogs-menu>
|
||||
|
||||
68
src/components/dialogs/dockerfile.riot
Normal file
@@ -0,0 +1,68 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2023 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/>.
|
||||
-->
|
||||
<dockerfile>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
|
||||
<div class="material-popup-title">Dockerfile</div>
|
||||
<div class="material-popup-content">
|
||||
<template each="{ (_, idx) in props.elements }">
|
||||
<template
|
||||
each="{ element in props.elements[props.elements.length - 1 - idx].filter(e => e.key === 'created_by') }"
|
||||
>
|
||||
<div class="instruction">
|
||||
<span class="keyword">{ element.value }</span>
|
||||
<span>{ ' ' + element.content }</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="material-popup-action">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="rgba(158,158,158,.4)"
|
||||
onClick="{ props.onClose }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Close
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<style>
|
||||
:host material-popup .popup material-button {
|
||||
margin-right: 1em;
|
||||
}
|
||||
:host material-popup .popup > .content {
|
||||
max-width: 75em;
|
||||
width: 80%;
|
||||
}
|
||||
:host .material-popup-content {
|
||||
background-color: var(--hover-background);
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
:host material-popup .popup > .content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
:host .instruction {
|
||||
font-family: 'Roboto Mono', monospace !important;
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
:host .instruction .keyword {
|
||||
color: var(--accent-text);
|
||||
}
|
||||
</style>
|
||||
</dockerfile>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,13 +16,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<remove-registry-url>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
|
||||
<div slot="title">Remove your Registry Server ?</div>
|
||||
<div slot="content">
|
||||
<div class="material-popup-title">Remove your Registry Server ?</div>
|
||||
<div class="material-popup-content">
|
||||
<ul class="list">
|
||||
<li each="{ url in getRegistryServers() }">
|
||||
<span>
|
||||
<material-button onClick="{ remove }" url="{ url }" rounded="true" waves-color="rgba(158,158,158,.4)"
|
||||
waves-center="true">
|
||||
<material-button
|
||||
onClick="{ remove(url) }"
|
||||
url="{ url }"
|
||||
text-color="var(--neutral-text)"
|
||||
color="inherit"
|
||||
waves-color="var(--hover-background)"
|
||||
waves-center="true"
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<span class="url">{ url }</span>
|
||||
@@ -30,33 +37,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
|
||||
<div class="material-popup-action">
|
||||
<material-button
|
||||
class="dialog-button"
|
||||
waves-color="rgba(158,158,158,.4)"
|
||||
onClick="{ props.onClose }"
|
||||
color="inherit"
|
||||
text-color="var(--primary-text)"
|
||||
>
|
||||
Close
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import { getRegistryServers, removeRegistryServers } from '../../scripts/utils';
|
||||
export default {
|
||||
remove(event) {
|
||||
const url = event.currentTarget.attributes.url && event.currentTarget.attributes.url.value;
|
||||
const registryServer = getRegistryServers().filter(e => e !== url);
|
||||
localStorage.setItem('registryServer', JSON.stringify(registryServer));
|
||||
setTimeout(() => this.update(), 100);
|
||||
remove(url) {
|
||||
return (event) => {
|
||||
removeRegistryServers(url);
|
||||
setTimeout(() => this.update(), 100);
|
||||
};
|
||||
},
|
||||
getRegistryServers
|
||||
}
|
||||
getRegistryServers,
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host material-popup .popup material-button {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
:host material-popup .popup material-button .content i.material-icons {
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</remove-registry-url>
|
||||
</remove-registry-url>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -17,69 +17,118 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<docker-registry-ui>
|
||||
<header>
|
||||
<material-navbar>
|
||||
<div class="logo">Docker Registry UI</div>
|
||||
<search-bar on-search="{ onSearch }"></search-bar>
|
||||
<dialogs-menu if="{props.singleRegistry !== 'true'}" on-notify="{ notifySnackbar }"
|
||||
on-server-change="{ onServerChange }"></dialogs-menu>
|
||||
<span class="logo">Docker Registry UI</span>
|
||||
<div class="menu">
|
||||
<search-bar on-search="{ onSearch }"></search-bar>
|
||||
<dialogs-menu
|
||||
if="{props.singleRegistry !== 'true'}"
|
||||
on-notify="{ notifySnackbar }"
|
||||
on-server-change="{ onServerChange }"
|
||||
default-registries="{ props.defaultRegistries }"
|
||||
read-only-registries="{ truthy(props.readOnlyRegistries) }"
|
||||
></dialogs-menu>
|
||||
</div>
|
||||
</material-navbar>
|
||||
</header>
|
||||
<main>
|
||||
<router base="#!">
|
||||
<route path="{baseRoute}">
|
||||
<catalog registry-url="{ state.registryUrl }" registry-name="{ state.name }"
|
||||
catalog-elements-limit="{ state.catalogElementsLimit }" on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }" on-authentication="{ onAuthentication }" />
|
||||
<catalog
|
||||
registry-url="{ state.registryUrl }"
|
||||
registry-name="{ state.name }"
|
||||
catalog-elements-limit="{ state.catalogElementsLimit }"
|
||||
on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }"
|
||||
on-authentication="{ onAuthentication }"
|
||||
show-catalog-nb-tags="{ truthy(props.showCatalogNbTags) }"
|
||||
></catalog>
|
||||
</route>
|
||||
<route path="{baseRoute}taglist/(.*)">
|
||||
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagListImage() }" show-content-digest="{ truthy(props.showContentDigest) }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }" on-authentication="{ onAuthentication }"></tag-list>
|
||||
<tag-list
|
||||
registry-url="{ state.registryUrl }"
|
||||
registry-name="{ state.name }"
|
||||
pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagListImage() }"
|
||||
show-content-digest="{ truthy(props.showContentDigest) }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }"
|
||||
on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }"
|
||||
on-authentication="{ onAuthentication }"
|
||||
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
|
||||
></tag-list>
|
||||
</route>
|
||||
<route path="{baseRoute}taghistory/(.*)">
|
||||
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
|
||||
on-authentication="{ onAuthentication }"></tag-history>
|
||||
<tag-history
|
||||
registry-url="{ state.registryUrl }"
|
||||
registry-name="{ state.name }"
|
||||
pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagHistoryImage() }"
|
||||
tag="{ router.getTagHistoryTag() }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }"
|
||||
on-notify="{ notifySnackbar }"
|
||||
on-authentication="{ onAuthentication }"
|
||||
history-custom-labels="{ stringToArray(props.historyCustomLabels) }"
|
||||
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
|
||||
></tag-history>
|
||||
</route>
|
||||
</router>
|
||||
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
|
||||
on-close="{ onAuthenticationClose }" on-authenticated="{ state.onAuthenticated }"
|
||||
opened="{ state.authenticationDialogOpened }"></registry-authentication>
|
||||
<registry-authentication
|
||||
realm="{ state.realm }"
|
||||
scope="{ state.scope }"
|
||||
service="{ state.service }"
|
||||
on-close="{ onAuthenticationClose }"
|
||||
on-authenticated="{ state.onAuthenticated }"
|
||||
opened="{ state.authenticationDialogOpened }"
|
||||
></registry-authentication>
|
||||
<error-page
|
||||
if="{ state.pageError }"
|
||||
code="{ state.pageError.code }"
|
||||
status="{ state.pageError.status }"
|
||||
message="{ state.pageError.message }"
|
||||
url="{ state.pageError.url }"
|
||||
></error-page>
|
||||
<material-snackbar message="{ state.snackbarMessage }" is-error="{ state.snackbarIsError }"></material-snackbar>
|
||||
</main>
|
||||
<footer>
|
||||
<material-footer>
|
||||
<a slot="logo" href="https://joxit.github.io/docker-registry-ui/">Docker Registry UI { version }</a>
|
||||
<ul slot="link-list">
|
||||
<material-footer mini="true">
|
||||
<a class="material-footer-logo" href="https://joxit.github.io/docker-registry-ui/">
|
||||
Docker Registry UI { version }
|
||||
</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/Joxit/docker-registry-ui">Contribute on GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">Privacy & Terms</a>
|
||||
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">License AGPL-3.0</a>
|
||||
</li>
|
||||
<li if="{ props.theme === 'auto' || props.theme === '' }">
|
||||
<material-switch
|
||||
on-change="{ onThemeChange }"
|
||||
checked="{ state.themeSwitch }"
|
||||
track-selected-color="var(--accent-text)"
|
||||
outline-selected-color="var(--accent-text)"
|
||||
>
|
||||
<i slot="thumb-icon" class="material-icons" style="color: white; font-size: 0.75em">wb_sunny</i>
|
||||
<i slot="thumb-selected-icon" class="material-icons" style="color: #79747e; font-size: 0.75em">
|
||||
brightness_2
|
||||
</i>
|
||||
</material-switch>
|
||||
</li>
|
||||
</ul>
|
||||
</material-footer>
|
||||
</footer>
|
||||
<script>
|
||||
import {
|
||||
version
|
||||
} from '../../package.json';
|
||||
import {
|
||||
Router,
|
||||
Route,
|
||||
} from '@riotjs/route'
|
||||
import { version } from '../../package.json';
|
||||
import { Router, Route } from '@riotjs/route';
|
||||
import Catalog from './catalog/catalog.riot';
|
||||
import TagList from './tag-list/tag-list.riot';
|
||||
import TagHistory from './tag-history/tag-history.riot';
|
||||
import DialogsMenu from './dialogs/dialogs-menu.riot';
|
||||
import SearchBar from './search-bar.riot'
|
||||
import {
|
||||
stripHttps,
|
||||
getRegistryServers,
|
||||
truthy
|
||||
} from '../scripts/utils';
|
||||
import SearchBar from './search-bar.riot';
|
||||
import ErrorPage from './error-page.riot';
|
||||
import { stripHttps, getRegistryServers, setRegistryServers, truthy, stringToArray } from '../scripts/utils';
|
||||
import router from '../scripts/router';
|
||||
import { loadTheme } from '../scripts/theme';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -89,85 +138,135 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
DialogsMenu,
|
||||
SearchBar,
|
||||
Router,
|
||||
Route
|
||||
Route,
|
||||
ErrorPage,
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
state.snackbarIsError = false;
|
||||
state.snackbarMessage = undefined;
|
||||
},
|
||||
onBeforeMount(props) {
|
||||
// props.singleRegistry === 'true' means old static version
|
||||
const registryUrl = props.registryUrl ||
|
||||
(props.singleRegistry === 'true' ? undefined : (router.getUrlQueryParam() || getRegistryServers(0))) ||
|
||||
(window.location.origin + window.location.pathname.replace(/\/+$/, ''));
|
||||
this.state.registryUrl = registryUrl.replace(/\/$/, '');
|
||||
if (
|
||||
(props.defaultRegistries && props.defaultRegistries.length > 0 && getRegistryServers().length === 0) ||
|
||||
truthy(props.readOnlyRegistries)
|
||||
) {
|
||||
setRegistryServers(props.defaultRegistries);
|
||||
}
|
||||
|
||||
window.onselectstart = (e) => {
|
||||
if (e.target && e.target.className) {
|
||||
return !['checkbox', 'checkmark', 'remove-tag'].find((elt) => e.target.className.indexOf(elt) >= 0);
|
||||
}
|
||||
};
|
||||
|
||||
// props.singleRegistry === 'true' means old static version
|
||||
const registryUrl =
|
||||
props.registryUrl ||
|
||||
(props.singleRegistry === 'true' ? undefined : router.getUrlQueryParam() || getRegistryServers(0)) ||
|
||||
window.location.origin + window.location.pathname.replace(/\/+$/, '');
|
||||
this.state.registryUrl = registryUrl.replace(/\/$/, '').replace(/index(\.html?)?$/, '');
|
||||
this.state.name = props.name || stripHttps(props.registryUrl);
|
||||
this.state.catalogElementsLimit = props.catalogElementsLimit || 100000;
|
||||
this.state.pullUrl = this.pullUrl(this.state.registryUrl, props.pullUrl);
|
||||
this.state.useControlCacheHeader = props.useControlCacheHeader;
|
||||
const theme = loadTheme(props, this.root.parentNode.style);
|
||||
this.state.themeSwitch = theme === 'dark';
|
||||
},
|
||||
onServerChange(registryUrl) {
|
||||
this.update({
|
||||
registryUrl,
|
||||
name: stripHttps(registryUrl),
|
||||
pullUrl: this.pullUrl(registryUrl),
|
||||
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.'
|
||||
})
|
||||
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.',
|
||||
});
|
||||
},
|
||||
onAuthentication(opts, onAuthenticated) {
|
||||
if (opts && opts.realm && opts.service && opts.scope) {
|
||||
const {
|
||||
realm,
|
||||
service,
|
||||
scope,
|
||||
} = opts;
|
||||
const req = new XMLHttpRequest()
|
||||
const { realm, service, scope } = opts;
|
||||
const req = new XMLHttpRequest();
|
||||
req.addEventListener('loadend', () => {
|
||||
try {
|
||||
const bearer = JSON.parse(req.responseText);
|
||||
onAuthenticated(bearer)
|
||||
onAuthenticated(bearer);
|
||||
} catch (e) {
|
||||
this.notifySnackbar(`Failed to log in: ${e.message}`, true)
|
||||
this.notifySnackbar(`Failed to log in: ${e.message}`, true);
|
||||
}
|
||||
})
|
||||
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
|
||||
req.send()
|
||||
});
|
||||
req.open('GET', `${realm}?service=${service}&scope=${scope}`);
|
||||
req.send();
|
||||
} else {
|
||||
onAuthenticated()
|
||||
onAuthenticated();
|
||||
}
|
||||
},
|
||||
onAuthenticationClose() {
|
||||
this.update({
|
||||
authenticationDialogOpened: false
|
||||
})
|
||||
authenticationDialogOpened: false,
|
||||
});
|
||||
},
|
||||
pullUrl(registryUrl, pullUrl) {
|
||||
const url = pullUrl ||
|
||||
(registryUrl && registryUrl.length > 0 && registryUrl) ||
|
||||
window.location.host;
|
||||
const url = pullUrl || (registryUrl && registryUrl.length > 0 && registryUrl) || window.location.host;
|
||||
return stripHttps(url);
|
||||
},
|
||||
notifySnackbar(message, isError) {
|
||||
if (typeof message === 'string') {
|
||||
this.update({
|
||||
snackbarMessage: message,
|
||||
snackbarIsError: isError || false
|
||||
snackbarIsError: isError || false,
|
||||
});
|
||||
} else if (message && message.code) {
|
||||
this.update({
|
||||
pageError: message,
|
||||
});
|
||||
setTimeout(() => delete this.state['pageError'], 1000);
|
||||
} else if (message && message.message) {
|
||||
this.update({
|
||||
snackbarMessage: message.message,
|
||||
snackbarIsError: message.isError
|
||||
snackbarIsError: message.isError,
|
||||
});
|
||||
}
|
||||
},
|
||||
onSearch(value) {
|
||||
this.update({
|
||||
filter: value
|
||||
})
|
||||
filter: value,
|
||||
});
|
||||
},
|
||||
onThemeChange(e) {
|
||||
const theme = e.target.checked ? 'dark' : 'light';
|
||||
loadTheme({ ...this.props, theme }, this.root.parentNode.style);
|
||||
this.update({ themeSwitch: e.target.checked });
|
||||
},
|
||||
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
|
||||
router,
|
||||
version,
|
||||
truthy
|
||||
}
|
||||
truthy,
|
||||
stringToArray,
|
||||
};
|
||||
</script>
|
||||
</docker-registry-ui>
|
||||
<style>
|
||||
material-navbar {
|
||||
height: 64px;
|
||||
color: var(--header-text);
|
||||
background-color: var(--header-background);
|
||||
}
|
||||
|
||||
material-navbar .menu {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
material-navbar .nav-wrapper .menu {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
material-footer {
|
||||
color: var(--footer-neutral-text);
|
||||
background-color: var(--footer-background);
|
||||
}
|
||||
material-footer .material-footer-logo {
|
||||
color: var(--footer-text);
|
||||
}
|
||||
|
||||
material-switch i {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</docker-registry-ui>
|
||||
|
||||
69
src/components/error-page.riot
Normal file
@@ -0,0 +1,69 @@
|
||||
<error-page>
|
||||
<div class="content">
|
||||
<h1 if="{ getStatusCode() }">{ getStatusCode() }</h1>
|
||||
<h2>{ props.code }</h2>
|
||||
<template if="{ props.code === 'CATALOG_NOT_FOUND' }">
|
||||
<p>We received a 404 status code from your registry.</p>
|
||||
<p>The contact point was <a href="{ props.url }">{ props.url }</a></p>
|
||||
<p>
|
||||
This may be caused by a misconfiguration of Docker Registry UI. Check the
|
||||
<a href="https://joxit.dev/docker-registry-ui/#faq">FAQ</a> and
|
||||
<a href="https://joxit.dev/docker-registry-ui/#available-options">Available options</a>
|
||||
</p>
|
||||
</template>
|
||||
<template if="{ props.code === 'MIXED_CONTENT' }">
|
||||
<p>
|
||||
<span>Mixed Content</span>: The page at `<a href="{ window.location.origin }">{ window.location.origin }</a>`
|
||||
was loaded over HTTPS, but requested an insecure server endpoint `<a href="{ new URL(props.url).origin }"
|
||||
>{ new URL(props.url).origin }</a
|
||||
>`.
|
||||
</p>
|
||||
<p>This request <span>may</span> has been blocked; the content must be served over HTTPS.</p>
|
||||
<p>
|
||||
You may unset the option `<span>REGISTRY_URL</span>` and set the registry server container URL in
|
||||
`<span>NGINX_PROXY_PASS_URL</span>`. It's usually the name of your container, and it should be on the shame
|
||||
network as the UI.
|
||||
</p>
|
||||
<p>You can check the issue <a href="https://github.com/Joxit/docker-registry-ui/issues/277">#277</a>.</p>
|
||||
</template>
|
||||
<template if="{ props.code === 'INCORRECT_URL' }">
|
||||
<p>`{ props.url }` does not seems to be a correct URL, should starts with http:// or https://.</p>
|
||||
</template>
|
||||
</div>
|
||||
<script>
|
||||
export default {
|
||||
getStatusCode() {
|
||||
const { props } = this;
|
||||
switch (props.code) {
|
||||
case 'CATALOG_NOT_FOUND':
|
||||
return '404';
|
||||
}
|
||||
},
|
||||
URL: window.URL,
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 20px;
|
||||
}
|
||||
:host .content {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
:host .content a {
|
||||
color: var(--accent-text);
|
||||
}
|
||||
:host .content a:visited {
|
||||
color: var(--accent-text);
|
||||
}
|
||||
:host .content p span {
|
||||
color: var(--accent-text);
|
||||
font-weight: 700;
|
||||
}
|
||||
:host .content h2 {
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</error-page>
|
||||
@@ -1,9 +1,12 @@
|
||||
<search-bar>
|
||||
<material-input placeholder="Search in page"></material-input>
|
||||
<material-input
|
||||
label="Search in page"
|
||||
text-color="var(--header-text)"
|
||||
label-color="var(--neutral-text)"
|
||||
color="var(--accent-text)"
|
||||
></material-input>
|
||||
<script>
|
||||
import {
|
||||
router
|
||||
} from '@riotjs/route';
|
||||
import { router } from '@riotjs/route';
|
||||
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
@@ -11,17 +14,29 @@
|
||||
let value = '';
|
||||
const notify = () => {
|
||||
if (value !== input.value) {
|
||||
props.onSearch(input.value.toLowerCase())
|
||||
props.onSearch(input.value.toLowerCase());
|
||||
}
|
||||
value = input.value;
|
||||
}
|
||||
};
|
||||
input.addEventListener('keyup', notify);
|
||||
router.on.value(() => {
|
||||
input.value = '';
|
||||
notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// F3 or CTRL + F
|
||||
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
|
||||
// already focused, fallback to default behavior
|
||||
if (document.activeElement === input) {
|
||||
return true;
|
||||
} else {
|
||||
e.preventDefault();
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function matchSearch(search, value) {
|
||||
return !search || (value && value.toLowerCase().indexOf(search) >= 0);
|
||||
@@ -29,10 +44,8 @@
|
||||
</script>
|
||||
<style>
|
||||
:host material-input {
|
||||
position: absolute;
|
||||
top: 0em;
|
||||
right: 64px;
|
||||
max-width: 20%;
|
||||
line-height: initial;
|
||||
min-width: 5em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
@@ -40,9 +53,5 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host material-input input {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</search-bar>
|
||||
</search-bar>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,17 +15,18 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-history-element class="{ state.key }">
|
||||
<div class="headline"><i class="material-icons">{ state.icon }</i>
|
||||
<p>{ state.name }</p>
|
||||
<div class="headline">
|
||||
<i if="{ state.key !== 'docker_version' }" class="material-icons">{ state.icon }</i>
|
||||
<span if="{ state.key === 'docker_version' }" id="docker_verion"></span>
|
||||
<span>{ state.name }</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="value" if="{ state.value }"> { state.value }</div>
|
||||
<div class="values value" each="{ value in state.values }" if="{ state.values }"> { value }</div>
|
||||
<div class="value" if="{ state.value }">{ state.value }</div>
|
||||
<div class="values value" each="{ value in state.values }" if="{ state.values }">{ value }</div>
|
||||
</div>
|
||||
<script>
|
||||
import {
|
||||
getHistoryIcon
|
||||
} from '../../scripts/utils';
|
||||
import { getHistoryIcon } from '../../scripts/utils';
|
||||
import dockerVersionIcon from '../../images/docker-logo.svg';
|
||||
export default {
|
||||
onBeforeStart(props, state) {
|
||||
state.key = props.entry.key;
|
||||
@@ -43,19 +44,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
onBeforeUpdate(props, state) {
|
||||
this.onBeforeStart(props, state);
|
||||
},
|
||||
onMounted(props, state) {
|
||||
const dockerVersion = this.$('#docker_verion');
|
||||
if (dockerVersion) {
|
||||
dockerVersion.innerHTML = dockerVersionIcon().firstElementChild.outerHTML;
|
||||
}
|
||||
},
|
||||
cleanName(name) {
|
||||
if (name === 'id') {
|
||||
return name;
|
||||
} else if (name === 'os') {
|
||||
return 'OS';
|
||||
} else if (name.startsWith('custom-label-')) {
|
||||
name = name.replace('custom-label-', '');
|
||||
}
|
||||
return name.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace('_', ' ')
|
||||
return name
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
||||
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host.Labels .value,
|
||||
@@ -68,10 +78,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
:host.docker_version .headline .material-icons {
|
||||
background-size: 24px auto;
|
||||
background-image: url("images/docker-logo.svg");
|
||||
background-repeat: no-repeat;
|
||||
:host.docker_version .headline .material-icons,
|
||||
:host.docker_version .headline #docker_verion {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
:host {
|
||||
@@ -91,16 +100,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
overflow-x: initial;
|
||||
}
|
||||
|
||||
:host .headline p {
|
||||
:host .headline {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
position: relative;
|
||||
display: inline;
|
||||
top: -4px;
|
||||
}
|
||||
:host .headline * {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
:host.id div.value {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</tag-history-element>
|
||||
</tag-history-element>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,38 +15,71 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-history>
|
||||
<material-card ref="tag-history-tag" class="tag-history header">
|
||||
<material-card>
|
||||
<div class="material-card-title-action">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ toTaglist }">
|
||||
<material-button
|
||||
text-color="var(--neutral-text)"
|
||||
color="inherit"
|
||||
waves-color="var(--hover-background)"
|
||||
waves-center="true"
|
||||
rounded="true"
|
||||
href="{ toTaglist() }"
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
History of { props.image }:{ props.tag } <i class="material-icons">history</i>
|
||||
</h2>
|
||||
<h2>History of { props.image }:{ props.tag } <i class="material-icons">history</i></h2>
|
||||
<material-button
|
||||
text-color="var(--accent-text)"
|
||||
color="inherit"
|
||||
waves-color="var(--hover-background)"
|
||||
waves-center="true"
|
||||
rounded="true"
|
||||
outlined
|
||||
onClick="{ showDockerfile }"
|
||||
>
|
||||
Dockerfile
|
||||
</material-button>
|
||||
</div>
|
||||
</material-card>
|
||||
<div if="{ !state.loadend }" class="spinner-wrapper">
|
||||
<material-spinner />
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
<dockerfile
|
||||
opened="{ state.showDockerfile }"
|
||||
on-close="{ onDockerfileClose }"
|
||||
elements="{ state.elements }"
|
||||
></dockerfile>
|
||||
|
||||
<material-tabs if="{ state.archs && state.loadend }" useLine="{ true }" tabs="{ state.archs }"
|
||||
onTabChanged="{ onTabChanged }" />
|
||||
<material-tabs
|
||||
if="{ state.archs && state.loadend }"
|
||||
color="var(--background)"
|
||||
text-color="var(--primary-text)"
|
||||
text-selected-color="var(--accent-text)"
|
||||
line-color="var(--background)"
|
||||
line-selected-color="var(--accent-text)"
|
||||
useLine="{ true }"
|
||||
tabs="{ state.archs }"
|
||||
onTabChanged="{ onTabChanged }"
|
||||
></material-tabs>
|
||||
|
||||
<material-card each="{ element in state.elements }" class="tag-history-element">
|
||||
<tag-history-element each="{ entry in element }" if="{ entry.value && entry.value.length > 0}" entry="{ entry }" />
|
||||
<material-card each="{ element in state.elements }">
|
||||
<tag-history-element
|
||||
each="{ entry in element }"
|
||||
if="{ entry.value && entry.value.length > 0}"
|
||||
entry="{ entry }"
|
||||
></tag-history-element>
|
||||
</material-card>
|
||||
<script>
|
||||
import {
|
||||
DockerImage
|
||||
} from '../../scripts/docker-image';
|
||||
import {
|
||||
bytesToSize
|
||||
} from '../../scripts/utils';
|
||||
import { DockerImage } from '../../scripts/docker-image';
|
||||
import { bytesToSize } from '../../scripts/utils';
|
||||
import Dockerfile from '../dialogs/dockerfile.riot';
|
||||
import router from '../../scripts/router';
|
||||
import TagHistoryElement from './tag-history-element.riot'
|
||||
import TagHistoryElement from './tag-history-element.riot';
|
||||
export default {
|
||||
components: {
|
||||
TagHistoryElement
|
||||
TagHistoryElement,
|
||||
Dockerfile,
|
||||
},
|
||||
onBeforeMount(props, state) {
|
||||
state.elements = [];
|
||||
@@ -54,9 +87,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
list: true,
|
||||
registryUrl: props.registryUrl,
|
||||
onNotify: props.onNotify,
|
||||
onAuthentication: props.onAuthentication
|
||||
onAuthentication: props.onAuthentication,
|
||||
useControlCacheHeader: props.useControlCacheHeader,
|
||||
});
|
||||
state.image.fillInfo()
|
||||
state.image.fillInfo();
|
||||
},
|
||||
onMounted(props, state) {
|
||||
state.image.on('blobs', this.processBlobs);
|
||||
@@ -64,16 +98,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
},
|
||||
onTabChanged(arch, idx) {
|
||||
const state = this.state;
|
||||
const {
|
||||
registryUrl,
|
||||
onNotify
|
||||
} = this.props;
|
||||
state.elements = []
|
||||
state.image.variants[idx] = state.image.variants[idx] ||
|
||||
const { registryUrl, onNotify, useControlCacheHeader } = this.props;
|
||||
state.elements = [];
|
||||
state.image.variants[idx] =
|
||||
state.image.variants[idx] ||
|
||||
new DockerImage(this.props.image, arch.digest, {
|
||||
list: false,
|
||||
registryUrl,
|
||||
onNotify
|
||||
onNotify,
|
||||
useControlCacheHeader,
|
||||
});
|
||||
if (state.image.variants[idx].blobs) {
|
||||
return this.processBlobs(state.image.variants[idx].blobs);
|
||||
@@ -83,48 +116,56 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
},
|
||||
processBlobs(blobs) {
|
||||
const state = this.state;
|
||||
const { historyCustomLabels } = this.props;
|
||||
|
||||
function exec(elt) {
|
||||
const guiElements = [];
|
||||
for (var attribute in elt) {
|
||||
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
|
||||
const value = elt[attribute];
|
||||
const guiElement = {
|
||||
"key": attribute,
|
||||
"value": modifySpecificAttributeTypes(attribute, value)
|
||||
};
|
||||
const guiElement = modifySpecificAttributeTypes(attribute, value);
|
||||
guiElements.push(guiElement);
|
||||
}
|
||||
}
|
||||
return guiElements.sort(eltSort);
|
||||
}
|
||||
const elements = new Array(blobs.history.length + 1);
|
||||
elements[0] = exec(getConfig(blobs));
|
||||
elements[0] = exec(getConfig(blobs, { historyCustomLabels }));
|
||||
blobs.history.forEach(function (elt, i) {
|
||||
elements[blobs.history.length - i] = exec(elt)
|
||||
elements[blobs.history.length - i] = exec(elt);
|
||||
});
|
||||
this.update({
|
||||
elements,
|
||||
loadend: true
|
||||
loadend: true,
|
||||
});
|
||||
},
|
||||
multiArchList(manifests) {
|
||||
manifests = manifests.manifests || manifests;
|
||||
const archs = manifests.map(function (manifest) {
|
||||
return {
|
||||
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ?
|
||||
manifest.platform.variant : ''),
|
||||
digest: manifest.digest
|
||||
}
|
||||
title:
|
||||
manifest.platform.os +
|
||||
'/' +
|
||||
manifest.platform.architecture +
|
||||
(manifest.platform.variant ? manifest.platform.variant : ''),
|
||||
digest: manifest.digest,
|
||||
};
|
||||
});
|
||||
this.update({
|
||||
archs
|
||||
archs,
|
||||
});
|
||||
},
|
||||
toTaglist() {
|
||||
router.taglist(this.props.image);
|
||||
}
|
||||
}
|
||||
return router.taglist(this.props.image);
|
||||
},
|
||||
showDockerfile() {
|
||||
console.log(this);
|
||||
this.update({ showDockerfile: true });
|
||||
},
|
||||
onDockerfileClose() {
|
||||
this.update({ showDockerfile: false });
|
||||
},
|
||||
};
|
||||
const eltIdx = function (e) {
|
||||
switch (e) {
|
||||
case 'created':
|
||||
@@ -152,49 +193,99 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return eltIdx(e1.key) - eltIdx(e2.key);
|
||||
};
|
||||
|
||||
const modifySpecificAttributeTypes = function (attribute, value) {
|
||||
switch (attribute) {
|
||||
case 'created':
|
||||
return new Date(value).toLocaleString();
|
||||
case 'created_by':
|
||||
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
|
||||
return (cmd && cmd[1]) || 'RUN'
|
||||
case 'size':
|
||||
return bytesToSize(value);
|
||||
case 'Entrypoint':
|
||||
case 'Cmd':
|
||||
return (value || []).join(' ');
|
||||
case 'Labels':
|
||||
return Object.keys(value || {}).map(function (elt) {
|
||||
return value[elt] ? elt + '=' + value[elt] : '';
|
||||
});
|
||||
case 'Volumes':
|
||||
case 'ExposedPorts':
|
||||
return Object.keys(value);
|
||||
const parseCreatedBy = (value) => {
|
||||
if (value.startsWith('COPY')) {
|
||||
return {
|
||||
value: 'COPY',
|
||||
content: value.replace(/^COPY /, ''),
|
||||
};
|
||||
}
|
||||
return value || '';
|
||||
let cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+) (.*)/);
|
||||
return {
|
||||
value: (cmd && cmd[1]) || 'RUN',
|
||||
content: (cmd && cmd[2]) || value.replace(/^\/bin\/sh *-c *(#\(nop\))?/, ''),
|
||||
};
|
||||
};
|
||||
|
||||
const getConfig = function (blobs) {
|
||||
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels',
|
||||
'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts'
|
||||
]
|
||||
.reduce(function (acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value && e === 'architecture' && blobs.variant) {
|
||||
acc[e] = value + blobs.variant;
|
||||
} else if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const modifySpecificAttributeTypes = function (key, value) {
|
||||
switch (key) {
|
||||
case 'created':
|
||||
return { key, value: new Date(value).toLocaleString() };
|
||||
case 'created_by':
|
||||
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+) (.*)/);
|
||||
return {
|
||||
key,
|
||||
...parseCreatedBy(value),
|
||||
};
|
||||
case 'size':
|
||||
return { key, value: bytesToSize(value) };
|
||||
case 'Entrypoint':
|
||||
case 'Cmd':
|
||||
return { key, value: (value || []).join(' ') };
|
||||
case 'Labels':
|
||||
return {
|
||||
key,
|
||||
value: Object.keys(value || {}).map(function (elt) {
|
||||
return value[elt] ? elt + '=' + value[elt] : '';
|
||||
}),
|
||||
};
|
||||
case 'Volumes':
|
||||
case 'ExposedPorts':
|
||||
return { key, value: Object.keys(value) };
|
||||
}
|
||||
return { key, value: value || '' };
|
||||
};
|
||||
|
||||
if (!res.author && (res.Labels && res.Labels.maintainer)) {
|
||||
const getConfig = function (blobs, { historyCustomLabels }) {
|
||||
const res = [
|
||||
'architecture',
|
||||
'User',
|
||||
'created',
|
||||
'docker_version',
|
||||
'os',
|
||||
'Cmd',
|
||||
'Entrypoint',
|
||||
'Env',
|
||||
'Labels',
|
||||
'User',
|
||||
'Volumes',
|
||||
'WorkingDir',
|
||||
'author',
|
||||
'id',
|
||||
'ExposedPorts',
|
||||
].reduce(function (acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value && e === 'architecture' && blobs.variant) {
|
||||
acc[e] = value + blobs.variant;
|
||||
} else if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!res.author && res.Labels && res.Labels.maintainer) {
|
||||
res.author = blobs.config.Labels.maintainer;
|
||||
delete res.Labels.maintainer;
|
||||
}
|
||||
|
||||
if (res.Labels) {
|
||||
historyCustomLabels
|
||||
.filter((label) => res.Labels[label])
|
||||
.forEach((label) => {
|
||||
res[`custom-label-${label}`] = res.Labels[label];
|
||||
delete res.Labels[label];
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
</script>
|
||||
</tag-history>
|
||||
<style>
|
||||
h2 {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</tag-history>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,16 +16,21 @@
|
||||
-->
|
||||
<copy-to-clipboard>
|
||||
<div class="copy-to-clipboard">
|
||||
<input style="display: none; width: 1px; height: 1px;" value="{ getDockerCmd(props) }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ copy }"
|
||||
title="Copy pull command.">
|
||||
<input style="display: none; width: 1px; height: 1px" value="{ getDockerCmd(props) }" />
|
||||
<material-button
|
||||
text-color="var(--neutral-text)"
|
||||
color="inherit"
|
||||
waves-color="var(--hover-background)"
|
||||
waves-center="true"
|
||||
onClick="{ copy }"
|
||||
title="Copy pull command."
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">content_copy</i>
|
||||
</material-button>
|
||||
</div>
|
||||
<script>
|
||||
import {
|
||||
ERROR_CAN_NOT_READ_CONTENT_DIGEST
|
||||
} from '../../scripts/utils';
|
||||
import { ERROR_CAN_NOT_READ_CONTENT_DIGEST } from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
this.load(props, state);
|
||||
@@ -37,13 +42,13 @@
|
||||
if (props.target === 'tag') {
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}:${props.image.tag}`;
|
||||
} else {
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.digest}`
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.contentDigest}`;
|
||||
}
|
||||
},
|
||||
load(props, state) {
|
||||
if (props.target !== 'tag' && !props.image.digest) {
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.update()
|
||||
if (props.target !== 'tag' && !props.image.contentDigest) {
|
||||
props.image.one('content-digest', (contentDigest) => {
|
||||
this.update();
|
||||
});
|
||||
props.image.trigger('get-content-digest');
|
||||
}
|
||||
@@ -60,8 +65,8 @@
|
||||
document.execCommand('copy');
|
||||
copyText.style.display = 'none';
|
||||
|
||||
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.')
|
||||
}
|
||||
}
|
||||
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</copy-to-clipboard>
|
||||
</copy-to-clipboard>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,7 +15,7 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<image-content-digest>
|
||||
<div title="{ getTitle(props.image, state.chars) }">{ getDigest(props.image, state.chars) }</div>
|
||||
<div title="{ getTitle(props.image, state.chars) }">{ getContentDigest(props.image, state.chars) }</div>
|
||||
<script>
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
@@ -25,12 +25,12 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
this.load(props, state);
|
||||
},
|
||||
load(props, state) {
|
||||
if (props.image.digest) {
|
||||
if (props.image.contentDigest) {
|
||||
return;
|
||||
}
|
||||
state.chars = -1;
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.digest = digest;
|
||||
props.image.one('content-digest', (contentDigest) => {
|
||||
this.contentDigest = contentDigest;
|
||||
props.image.on('content-digest-chars', this.onResize);
|
||||
props.image.trigger('get-content-digest-chars');
|
||||
});
|
||||
@@ -39,22 +39,22 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
onResize(chars) {
|
||||
if (chars !== this.state.chars) {
|
||||
this.update({
|
||||
chars
|
||||
chars,
|
||||
});
|
||||
}
|
||||
},
|
||||
getTitle(image, chars) {
|
||||
return chars >= 70 ? '' : (image.digest || '');
|
||||
return chars >= 70 ? '' : image.contentDigest || '';
|
||||
},
|
||||
getDigest(image, chars) {
|
||||
getContentDigest(image, chars) {
|
||||
if (chars >= 70) {
|
||||
return image.digest || '';
|
||||
return image.contentDigest || '';
|
||||
} else if (chars <= 0) {
|
||||
return '';
|
||||
} else {
|
||||
return image.digest && image.digest.slice(0, chars) + '...';
|
||||
return image.contentDigest && image.contentDigest.slice(0, chars) + '...';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</image-content-digest>
|
||||
</image-content-digest>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,27 +15,35 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<image-date>
|
||||
<div title="Creation date { getLocalDate(props.image) }">{ getDate(props.image) } ago</div>
|
||||
<div title="Creation date { getLocalDate(props.image) }">{ getDate(props.image) }</div>
|
||||
<script>
|
||||
import {
|
||||
dateFormat,
|
||||
} from '../../scripts/utils';
|
||||
import { dateFormat } from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props) {
|
||||
props.image.one('creation-date', (date) => {
|
||||
this.update({
|
||||
date: date,
|
||||
localDate: date.toLocaleString()
|
||||
this.loadCreationDate(props);
|
||||
},
|
||||
onUpdated(props) {
|
||||
this.loadCreationDate(props);
|
||||
},
|
||||
loadCreationDate(props) {
|
||||
if (!props.image.creationDate && !props.image.ociImage) {
|
||||
props.image.one('creation-date', (date) => {
|
||||
this.update({
|
||||
date: date,
|
||||
localDate: date && date.toLocaleString(),
|
||||
});
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-date');
|
||||
props.image.trigger('get-date');
|
||||
}
|
||||
},
|
||||
getDate(image) {
|
||||
return dateFormat(image.creationDate)
|
||||
return !image.ociImage ? `${dateFormat(image.creationDate)} ago` : 'Not Available';
|
||||
},
|
||||
getLocalDate(image) {
|
||||
return (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
|
||||
}
|
||||
}
|
||||
return !image.ociImage
|
||||
? (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
|
||||
: 'unavailable on OCI index/Buildkit export cache';
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</image-date>
|
||||
</image-date>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -17,9 +17,7 @@
|
||||
<image-size>
|
||||
<div title="Compressed size of your image.">{ getImageSize(props.image) }</div>
|
||||
<script>
|
||||
import {
|
||||
bytesToSize,
|
||||
} from '../../scripts/utils';
|
||||
import { bytesToSize } from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
this.load(props, state);
|
||||
@@ -33,15 +31,14 @@
|
||||
}
|
||||
props.image.on('size', (size) => {
|
||||
this.update({
|
||||
size
|
||||
size,
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-size');
|
||||
|
||||
},
|
||||
getImageSize(image) {
|
||||
return bytesToSize(image.size)
|
||||
}
|
||||
}
|
||||
return bytesToSize(image.size);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</image-size>
|
||||
</image-size>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -21,11 +21,11 @@
|
||||
onMounted(props) {
|
||||
props.image.on('sha256', (sha256) => {
|
||||
this.update({
|
||||
sha256: sha256.substring(0, 19)
|
||||
sha256: sha256 && sha256.substring(0, 19),
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-sha256');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</image-tag>
|
||||
</image-tag>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,16 +15,57 @@ 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>
|
||||
<div class="conatianer">
|
||||
<div class="container">
|
||||
<div class="pagination-centered">
|
||||
<material-button waves-color="rgba(158,158,158,.4)" each="{p in props.pages}"
|
||||
<material-button
|
||||
aria-label="page-{ p.page }"
|
||||
color="{ p.current ? 'var(--accent-text)' : 'rgba(0, 0, 0, 0 )' }"
|
||||
text-color="{ p.current ? 'var(--accent-text)' : 'var(--primary-text)' }"
|
||||
waves-color="rgba(158,158,158,.4)"
|
||||
each="{ (p, idx) in props.pages}"
|
||||
class="{ p.current ? 'current' : ''} { p['space-left'] ? 'space-left' : '' } { p['space-right'] ? 'space-right' : ''}"
|
||||
onClick="{() => props.onPageUpdate(p.page)}">
|
||||
onClick="{(e) => props.onPageUpdate(idx)}"
|
||||
outlined
|
||||
>
|
||||
<i if="{ p.icon }" class="material-icons">{ p.icon }</i>
|
||||
<div if="{ !p.icon }">{ p.page }</div>
|
||||
<template if="{ !p.icon }">{ p.page }</template>
|
||||
</material-button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</pagination>
|
||||
<script></script>
|
||||
<style>
|
||||
:host .container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:host .container .pagination-centered {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
:host material-button > :first-child {
|
||||
padding: 0;
|
||||
min-width: 40px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
:host material-button > :first-child .content {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
:host material-button.current > :first-child.space-left {
|
||||
margin-left: 85px;
|
||||
}
|
||||
|
||||
:host material-button.current > :first-child.space-right {
|
||||
margin-right: 85px;
|
||||
}
|
||||
|
||||
:host material-button .content i.material-icons {
|
||||
height: unset;
|
||||
}
|
||||
</style>
|
||||
</pagination>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,29 +15,37 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<remove-image>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image."
|
||||
if="{ !props.multiDelete }" disabled="{ !state.digest }" onClick="{ deleteImage }">
|
||||
<material-button
|
||||
text-color="var(--neutral-text)"
|
||||
color="inherit"
|
||||
waves-color="var(--hover-background)"
|
||||
waves-center="true"
|
||||
title="This will delete the image."
|
||||
if="{ !props.multiDelete }"
|
||||
disabled="{ !state.contentDigest }"
|
||||
onClick="{ deleteImage }"
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<material-checkbox if="{ props.multiDelete }" title="Select this tag to delete it." disabled="{ !state.digest }"
|
||||
onChange="{ handleCheckboxChange }" checked="{ state.checked }">
|
||||
<material-checkbox
|
||||
if="{ props.multiDelete }"
|
||||
title="Select this tag to delete it."
|
||||
disabled="{ !state.contentDigest }"
|
||||
onChange="{ handleCheckboxChange }"
|
||||
checked="{ state.checked }"
|
||||
>
|
||||
</material-checkbox>
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import router from '../../scripts/router'
|
||||
import {
|
||||
ACTION_CHECK_TO_DELETE,
|
||||
ACTION_UNCHECK_TO_DELETE,
|
||||
ACTION_DELETE_IMAGE
|
||||
} from './tag-table.riot';
|
||||
import { Http } from '../../scripts/http';
|
||||
import router from '../../scripts/router';
|
||||
import { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE } from './tag-table.riot';
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
state.checked = props.checked;
|
||||
props.image.one('content-digest', (digest) => {
|
||||
props.image.one('content-digest', (contentDigest) => {
|
||||
this.update({
|
||||
digest
|
||||
contentDigest,
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -50,10 +58,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
deleteImage() {
|
||||
this.props.handleCheckboxChange(ACTION_DELETE_IMAGE, this.props.image);
|
||||
},
|
||||
handleCheckboxChange(checked) {
|
||||
const action = checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
|
||||
this.props.handleCheckboxChange(action, this.props.image);
|
||||
}
|
||||
}
|
||||
handleCheckboxChange(event) {
|
||||
const action = event.target.checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
|
||||
this.props.handleCheckboxChange(action, this.props.image, event.shiftKey);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</remove-image>
|
||||
</remove-image>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,16 +15,36 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-history-button>
|
||||
<material-button title="This will show the history of given tag" waves-center="true" rounded="true" waves-color="#ddd"
|
||||
onClick="{ routeToHistory }">
|
||||
<material-button
|
||||
title="{ buttonTittle() }"
|
||||
text-color="var(--neutral-text)"
|
||||
color="inherit"
|
||||
waves-color="var(--hover-background)"
|
||||
waves-center="true"
|
||||
href="{ routeToHistory() }"
|
||||
disabled="{ props.image.ociImage }"
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">history</i>
|
||||
</material-button>
|
||||
<script>
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
onMounted(props) {
|
||||
props.image.one('oci-image', () => {
|
||||
this.update();
|
||||
});
|
||||
},
|
||||
buttonTittle() {
|
||||
return !this.props.image.ociImage
|
||||
? 'This will show the history of given tag'
|
||||
: 'History is unavailable on OCI index/Buildkit export cache';
|
||||
},
|
||||
routeToHistory() {
|
||||
router.history(this.props.image.name, this.props.image.tag)
|
||||
}
|
||||
}
|
||||
if (!this.props.image.ociImage) {
|
||||
return router.history(this.props.image.name, this.props.image.tag);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</tag-history-button>
|
||||
</tag-history-button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,15 +16,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-list>
|
||||
<material-card class="header">
|
||||
<div class="material-card-title-action ">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ router.home }">
|
||||
<div class="material-card-title-action">
|
||||
<material-button
|
||||
text-color="var(--neutral-text)"
|
||||
color="inherit"
|
||||
waves-color="var(--hover-background)"
|
||||
waves-center="true"
|
||||
href="{ router.home() }"
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
Tags of { props.image }
|
||||
<div class="source-hint">
|
||||
Sourced from { state.registryName + '/' + props.image }
|
||||
</div>
|
||||
<div class="source-hint">Sourced from { state.registryName + '/' + props.image }</div>
|
||||
<div class="item-count">{ state.tags.length } tags</div>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -36,30 +41,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||
|
||||
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
|
||||
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
|
||||
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"
|
||||
on-authentication="{ props.onAuthentication }">
|
||||
<tag-table
|
||||
if="{ state.loadend }"
|
||||
tags="{state.tags}"
|
||||
asc="{state.asc}"
|
||||
page="{ state.page }"
|
||||
show-content-digest="{props.showContentDigest}"
|
||||
is-image-remove-activated="{props.isImageRemoveActivated}"
|
||||
onReverseOrder="{ onReverseOrder }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
filter-results="{ props.filterResults }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
>
|
||||
</tag-table>
|
||||
|
||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import {
|
||||
DockerImage,
|
||||
compare
|
||||
} from '../../scripts/docker-image';
|
||||
import {
|
||||
getNumPages,
|
||||
getPageLabels
|
||||
} from '../../scripts/utils'
|
||||
import Pagination from './pagination.riot'
|
||||
import TagTable from './tag-table.riot'
|
||||
import router from '../../scripts/router'
|
||||
import { Http } from '../../scripts/http';
|
||||
import { DockerImage, compare } from '../../scripts/docker-image';
|
||||
import { getNumPages, getPageLabels } from '../../scripts/utils';
|
||||
import Pagination from './pagination.riot';
|
||||
import TagTable from './tag-table.riot';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
components: {
|
||||
Pagination,
|
||||
@@ -71,11 +77,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
tags: [],
|
||||
loadend: false,
|
||||
asc: true,
|
||||
page: router.getPageQueryParam() || 1
|
||||
}
|
||||
page: router.getPageQueryParam() || 1,
|
||||
};
|
||||
},
|
||||
onMounted(props, state) {
|
||||
this.display(props, state)
|
||||
this.display(props, state);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
// this may be run before the final document size is available, so schedule
|
||||
// a correction once everything is set up.
|
||||
@@ -85,23 +91,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
state.tags = [];
|
||||
const self = this;
|
||||
const oReq = new Http({
|
||||
onAuthentication: props.onAuthentication
|
||||
onAuthentication: props.onAuthentication,
|
||||
});
|
||||
oReq.addEventListener('load', function () {
|
||||
if (this.status == 200) {
|
||||
if (this.status === 200) {
|
||||
const tags = (JSON.parse(this.responseText).tags || [])
|
||||
.map(tag => new DockerImage(props.image, tag, {
|
||||
registryUrl: props.registryUrl,
|
||||
onNotify: props.onNotify,
|
||||
onAuthentication: props.onAuthentication
|
||||
}))
|
||||
.map(
|
||||
(tag) =>
|
||||
new DockerImage(props.image, tag, {
|
||||
list: true,
|
||||
registryUrl: props.registryUrl,
|
||||
onNotify: props.onNotify,
|
||||
onAuthentication: props.onAuthentication,
|
||||
useControlCacheHeader: props.useControlCacheHeader,
|
||||
})
|
||||
)
|
||||
.sort(compare);
|
||||
window.requestAnimationFrame(self.onResize);
|
||||
self.update({
|
||||
page: Math.min(state.page, getNumPages(tags)),
|
||||
tags
|
||||
})
|
||||
} else if (this.status == 404) {
|
||||
tags,
|
||||
});
|
||||
} else if (this.status === 404) {
|
||||
self.props.onNotify('Server not found', true);
|
||||
} else {
|
||||
self.props.onNotify(this.responseText, true);
|
||||
@@ -113,7 +124,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
});
|
||||
oReq.addEventListener('loadend', function () {
|
||||
self.update({
|
||||
loadend: true
|
||||
loadend: true,
|
||||
});
|
||||
});
|
||||
oReq.open('GET', props.registryUrl + '/v2/' + props.image + '/tags/list');
|
||||
@@ -121,9 +132,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
state.asc = true;
|
||||
},
|
||||
|
||||
onPageUpdate(page) {
|
||||
onPageUpdate(idx) {
|
||||
const labels = getPageLabels(this.state.page, getNumPages(this.state.tags));
|
||||
const page = labels[idx].page;
|
||||
this.update({
|
||||
page: page
|
||||
page: page,
|
||||
});
|
||||
router.updatePageQueryParam(page);
|
||||
},
|
||||
@@ -143,8 +156,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// SHA256:12345678 + scaled between 1024 and 1440px
|
||||
chars = 15 + 56 * ((innerWidth - 1024) / 416);
|
||||
}
|
||||
if (max > 20) chars -= (max - 20);
|
||||
chars = Math.floor(chars)
|
||||
if (max > 20) chars -= max - 20;
|
||||
chars = Math.floor(chars);
|
||||
this.state.tags.map(function (image) {
|
||||
image.trigger('content-digest-chars', chars);
|
||||
});
|
||||
@@ -162,7 +175,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
},
|
||||
getPageLabels,
|
||||
getNumPages,
|
||||
router
|
||||
}
|
||||
router,
|
||||
};
|
||||
</script>
|
||||
</tag-list>
|
||||
</tag-list>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -15,39 +15,63 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-table>
|
||||
<confirm-delete-image opened="{ state.confirmDeleteImage }" on-click="{ onConfirmDeleteImageClick }"
|
||||
registry-url="{ props.registryUrl }" on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }"
|
||||
tags="{ props.tags }" to-delete="{ state.toDelete }"></confirm-delete-image>
|
||||
<confirm-delete-image
|
||||
opened="{ state.confirmDeleteImage }"
|
||||
on-click="{ onConfirmDeleteImageClick }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
tags="{ props.tags }"
|
||||
to-delete="{ state.toDelete }"
|
||||
></confirm-delete-image>
|
||||
<material-card class="taglist">
|
||||
<table style="border: none;">
|
||||
<table style="border: none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="creation-date { (state.desc && state.orderType === 'date') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
|
||||
onclick="{() => onPageReorder('date') }">
|
||||
onclick="{() => onPageReorder('date') }"
|
||||
>
|
||||
Creation date
|
||||
</th>
|
||||
<th
|
||||
class="image-size { (state.desc && state.orderType === 'size') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
|
||||
onclick="{() => onPageReorder('size') }">
|
||||
onclick="{() => onPageReorder('size') }"
|
||||
>
|
||||
Size
|
||||
</th>
|
||||
<th id="image-content-digest-header" if="{ props.showContentDigest }">Content Digest</th>
|
||||
|
||||
<th id="image-tag-header"
|
||||
<th
|
||||
id="image-tag-header"
|
||||
class="{ props.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
|
||||
onclick="{ onReverseOrder }">Tag
|
||||
onclick="{ onReverseOrder }"
|
||||
>
|
||||
Tag
|
||||
</th>
|
||||
<th class="show-tag-history">History</th>
|
||||
<th class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
|
||||
if="{ props.isImageRemoveActivated }">
|
||||
<material-checkbox class="indeterminate" checked="{ state.multiDelete }"
|
||||
<th
|
||||
class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
|
||||
if="{ props.isImageRemoveActivated }"
|
||||
>
|
||||
<material-checkbox
|
||||
class="indeterminate"
|
||||
checked="{ state.multiDelete }"
|
||||
if="{ state.toDelete.size === 0 || state.singleDeleteAction }"
|
||||
title="Toggle multi-delete. Alt+Click to select all tags." onChange="{ onRemoveImageHeaderChange }">
|
||||
title="Toggle multi-delete. Alt+Click to select all tags."
|
||||
onChange="{ onRemoveImageHeaderChange }"
|
||||
>
|
||||
</material-checkbox>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd"
|
||||
title="This will delete selected images." onClick="{ deleteImages }"
|
||||
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }">
|
||||
<material-button
|
||||
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }"
|
||||
waves-center="true"
|
||||
color="inherit"
|
||||
text-color="var(--neutral-background)"
|
||||
waves-color="var(--hover-background)"
|
||||
title="This will delete selected images."
|
||||
onClick="{ deleteImages }"
|
||||
icon
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
</th>
|
||||
@@ -56,37 +80,49 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<tbody>
|
||||
<tr each="{ image in getPage(props.tags, props.page) }" if="{ matchSearch(props.filterResults, image.tag) }">
|
||||
<td class="creation-date">
|
||||
<image-date image="{ image }" />
|
||||
<image-date image="{ image }"></image-date>
|
||||
</td>
|
||||
<td class="image-size">
|
||||
<image-size image="{ image }" />
|
||||
<image-size image="{ image }"></image-size>
|
||||
</td>
|
||||
<td if="{ props.showContentDigest }">
|
||||
<image-content-digest image="{ image }" />
|
||||
<copy-to-clipboard target="digest" image="{ image }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" />
|
||||
<image-content-digest image="{ image }"></image-content-digest>
|
||||
<copy-to-clipboard
|
||||
target="digest"
|
||||
image="{ image }"
|
||||
pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
></copy-to-clipboard>
|
||||
</td>
|
||||
<td>
|
||||
<image-tag image="{ image }" />
|
||||
<copy-to-clipboard target="tag" image="{ image }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" />
|
||||
<image-tag image="{ image }"></image-tag>
|
||||
<copy-to-clipboard
|
||||
target="tag"
|
||||
image="{ image }"
|
||||
pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
></copy-to-clipboard>
|
||||
</td>
|
||||
<td class="show-tag-history">
|
||||
<tag-history-button image="{ image }" />
|
||||
<tag-history-button image="{ image }"></tag-history-button>
|
||||
</td>
|
||||
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
|
||||
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
|
||||
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
|
||||
on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }" />
|
||||
<remove-image
|
||||
multi-delete="{ state.multiDelete }"
|
||||
image="{ image }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
handleCheckboxChange="{ onRemoveImageChange }"
|
||||
checked="{ state.toDelete.has(image) }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
></remove-image>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</material-card>
|
||||
<script>
|
||||
import {
|
||||
getPage,
|
||||
} from '../../scripts/utils';
|
||||
import { getPage } from '../../scripts/utils';
|
||||
import ImageDate from './image-date.riot';
|
||||
import ImageSize from './image-size.riot';
|
||||
import ImageTag from './image-tag.riot';
|
||||
@@ -94,9 +130,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import CopyToClipboard from './copy-to-clipboard.riot';
|
||||
import TagHistoryButton from './tag-history-button.riot';
|
||||
import RemoveImage from './remove-image.riot';
|
||||
import {
|
||||
matchSearch
|
||||
} from '../search-bar.riot';
|
||||
import { matchSearch } from '../search-bar.riot';
|
||||
import ConfirmDeleteImage from '../dialogs/confirm-delete-image.riot';
|
||||
const ACTION_CHECK_TO_DELETE = 'CHECK';
|
||||
const ACTION_UNCHECK_TO_DELETE = 'UNCHECK';
|
||||
@@ -118,18 +152,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
toDelete: new Set(),
|
||||
multiDelete: false,
|
||||
page: props.page,
|
||||
}
|
||||
};
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
if (state.page !== props.page) {
|
||||
state.toDelete.clear();
|
||||
}
|
||||
state.page = props.page
|
||||
state.page = props.page;
|
||||
},
|
||||
deleteImages() {
|
||||
this.update({
|
||||
confirmDeleteImage: true
|
||||
})
|
||||
confirmDeleteImage: true,
|
||||
});
|
||||
},
|
||||
onConfirmDeleteImageClick() {
|
||||
if (this.state.singleDeleteAction) {
|
||||
@@ -137,34 +171,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
this.update({
|
||||
singleDeleteAction: false,
|
||||
confirmDeleteImage: false
|
||||
})
|
||||
confirmDeleteImage: false,
|
||||
});
|
||||
},
|
||||
onRemoveImageHeaderChange(checked, event) {
|
||||
onRemoveImageHeaderChange(event) {
|
||||
if (event.altKey === true) {
|
||||
const tags = getPage(this.props.tags, this.props.page);
|
||||
tags.filter(image => matchSearch(this.props.filterResults, image.tag))
|
||||
.forEach(tag => this.state.toDelete.add(tag));
|
||||
tags
|
||||
.filter((image) => matchSearch(this.props.filterResults, image.tag))
|
||||
.forEach((tag) => this.state.toDelete.add(tag));
|
||||
this.update({
|
||||
multiDelete: true,
|
||||
toDelete: this.state.toDelete
|
||||
})
|
||||
toDelete: this.state.toDelete,
|
||||
slectedImage: undefined,
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
multiDelete: checked
|
||||
})
|
||||
multiDelete: event.target.checked,
|
||||
slectedImage: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
onRemoveImageChange(action, image) {
|
||||
onRemoveImageChange(action, image, shiftKey) {
|
||||
let confirmDeleteImage = false;
|
||||
let singleDeleteAction = false;
|
||||
let slectedImage = undefined;
|
||||
switch (action) {
|
||||
case ACTION_CHECK_TO_DELETE: {
|
||||
this.state.toDelete.add(image);
|
||||
if (shiftKey) {
|
||||
slectedImage = this.supportShiftKey(image, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_UNCHECK_TO_DELETE: {
|
||||
this.state.toDelete.delete(image);
|
||||
if (shiftKey) {
|
||||
slectedImage = this.supportShiftKey(image, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_DELETE_IMAGE: {
|
||||
@@ -177,8 +221,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
this.update({
|
||||
toDelete: this.state.toDelete,
|
||||
confirmDeleteImage,
|
||||
singleDeleteAction
|
||||
})
|
||||
singleDeleteAction,
|
||||
slectedImage,
|
||||
});
|
||||
},
|
||||
supportShiftKey(selectedImage, addOrRemove) {
|
||||
if (!this.state.slectedImage) {
|
||||
return selectedImage;
|
||||
} else {
|
||||
let shouldChange = false;
|
||||
const tags = getPage(this.props.tags, this.props.page);
|
||||
tags
|
||||
.filter((image) => {
|
||||
if (image == this.state.slectedImage || image == selectedImage) {
|
||||
shouldChange = !shouldChange;
|
||||
return true;
|
||||
}
|
||||
return shouldChange;
|
||||
})
|
||||
.forEach((image) => {
|
||||
if (addOrRemove) {
|
||||
this.state.toDelete.add(image);
|
||||
} else {
|
||||
this.state.toDelete.delete(image);
|
||||
}
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
onReverseOrder() {
|
||||
this.state.orderType = null;
|
||||
@@ -188,30 +257,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
onPageReorder(type) {
|
||||
this.update({
|
||||
orderType: type,
|
||||
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc
|
||||
})
|
||||
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc,
|
||||
});
|
||||
},
|
||||
getPage(tags, page) {
|
||||
const sortedTags = getPage(tags, page);
|
||||
if (this.state.orderType === 'date') {
|
||||
sortedTags.sort((e1, e2) =>
|
||||
!this.state.desc ?
|
||||
e2.creationDate.getTime() - e1.creationDate.getTime() :
|
||||
e1.creationDate.getTime() - e2.creationDate.getTime());
|
||||
!this.state.desc
|
||||
? (e2.creationDate?.getTime()||0) - (e1.creationDate?.getTime()||0)
|
||||
: (e1.creationDate?.getTime()||0) - (e2.creationDate?.getTime()||0)
|
||||
);
|
||||
} else if (this.state.orderType === 'size') {
|
||||
sortedTags.sort((e1, e2) =>
|
||||
!this.state.desc ?
|
||||
e2.size - e1.size :
|
||||
e1.size - e2.size);
|
||||
sortedTags.sort((e1, e2) => (!this.state.desc ? e2.size - e1.size : e1.size - e2.size));
|
||||
}
|
||||
return sortedTags;
|
||||
},
|
||||
matchSearch
|
||||
}
|
||||
export {
|
||||
ACTION_CHECK_TO_DELETE,
|
||||
ACTION_UNCHECK_TO_DELETE,
|
||||
ACTION_DELETE_IMAGE
|
||||
}
|
||||
matchSearch,
|
||||
};
|
||||
export { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE };
|
||||
</script>
|
||||
</tag-table>
|
||||
</tag-table>
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="-5.724 -43.601 730 600" fill="#777">
|
||||
<path
|
||||
d="m595.942,422.343 c -7.543,0.119 -13.562,6.331 -13.443,13.875 0.119,7.544 6.332,13.562 13.875,13.443 7.495,-0.118 13.494,-6.254 13.445,-13.75 -0.085,-7.578 -6.297,-13.652 -13.875,-13.568 0,0 -10e-4,0 0,0 m 0,24.398 c -5.975,0.272 -11.039,-4.352 -11.311,-10.326 -0.271,-5.976 4.352,-11.04 10.327,-11.312 5.975,-0.271 11.039,4.352 11.311,10.327 0.01,0.19 0.013,0.382 0.011,0.573 0.204,5.723 -4.27,10.527 -9.992,10.731 -0.115,0.005 -0.23,0.007 -0.346,0.007"/>
|
||||
<path
|
||||
d="m599.081,436.342 v -0.185 c 1.512,-0.292 2.65,-1.544 2.8,-3.076 0.057,-1.175 -0.432,-2.311 -1.323,-3.077 -1.445,-0.765 -3.076,-1.106 -4.707,-0.984 -1.743,-0.024 -3.484,0.12 -5.2,0.431 v 13.538 h 3.077 v -5.446 h 1.477 c 1.754,0 2.554,0.646 2.83,2.154 0.184,1.143 0.536,2.252 1.047,3.292 h 3.415 c -0.53,-1.062 -0.873,-2.207 -1.016,-3.385 -0.138,-1.473 -1.088,-2.744 -2.462,-3.292 m -3.723,-0.985 h -1.508 v -3.908 c 0.583,-0.069 1.172,-0.069 1.754,0 1.97,0 2.893,0.831 2.893,2.062 0,1.231 -1.415,2 -3.076,2"/>
|
||||
<path
|
||||
d="M707.494,193.557c-1.938-1.539-20.029-15.199-58.181-15.199c-10.074,0.044-20.127,0.908-30.061,2.584 c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908l-6.738,9.723c-8.438,13.061-14.598,27.459-18.214,42.582 c-6.831,28.891-2.677,56.027,11.999,79.226c-17.722,9.876-46.151,12.307-51.904,12.522H22.367 c-12.294,0.017-22.27,9.952-22.337,22.245c-0.549,41.234,6.437,82.222,20.614,120.946c16.214,42.521,40.336,73.842,71.719,93.01 c35.167,21.537,92.302,33.844,157.067,33.844c29.258,0.092,58.461-2.556,87.226-7.907c39.986-7.342,78.463-21.318,113.839-41.352 c29.149-16.88,55.383-38.354,77.688-63.596c37.29-42.213,59.505-89.226,76.026-131.007c2.215,0,4.431,0,6.584,0 c40.828,0,65.935-16.338,79.78-30.029c9.201-8.732,16.384-19.369,21.045-31.167l2.923-8.553L707.494,193.557z"/>
|
||||
<path
|
||||
d="M65.995,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.476-5.538 c-0.01,0-0.021,0-0.031,0H65.995c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C60.488,226.443,62.953,228.909,65.995,228.909L65.995,228.909"/>
|
||||
<path
|
||||
d="M152.913,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.059,0-5.538,2.479-5.538,5.538v56.181C147.392,226.448,149.866,228.909,152.913,228.909"/>
|
||||
<path
|
||||
d="M241.153,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C235.646,226.443,238.112,228.909,241.153,228.909L241.153,228.909"/>
|
||||
<path
|
||||
d="M328.348,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C322.841,226.443,325.307,228.909,328.348,228.909L328.348,228.909"/>
|
||||
<path
|
||||
d="M152.913,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.046,0-5.521,2.46-5.538,5.507v56.181C147.392,145.597,149.861,148.066,152.913,148.083"/>
|
||||
<path
|
||||
d="M241.153,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C235.646,145.591,238.107,148.066,241.153,148.083"/>
|
||||
<path
|
||||
d="M328.348,148.083h63.073c3.052-0.017,5.521-2.486,5.538-5.538V86.364c-0.017-3.047-2.491-5.507-5.538-5.507h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C322.841,145.591,325.302,148.066,328.348,148.083"/>
|
||||
<path
|
||||
d="M328.348,67.227h63.073c3.047,0,5.521-2.461,5.538-5.507V5.507C396.942,2.46,394.468,0,391.421,0h-63.073 c-3.042,0-5.507,2.465-5.507,5.507l0,0v56.212C322.841,64.761,325.307,67.227,328.348,67.227"/>
|
||||
<path
|
||||
d="M416.312,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.041,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C410.805,226.443,413.271,228.909,416.312,228.909"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 730 730" fill="var(--neutral-text)">
|
||||
<path d="M603.672 530.973c-7.543.12-13.562 6.331-13.443 13.875.119 7.544 6.332 13.562 13.875 13.443 7.495-.118 13.494-6.254 13.445-13.75-.085-7.578-6.297-13.652-13.875-13.568 0 0-.001 0 0 0m0 24.398c-5.975.272-11.04-4.352-11.311-10.326-.271-5.976 4.352-11.04 10.327-11.312 5.975-.27 11.039 4.352 11.31 10.327.01.19.014.382.012.573.204 5.723-4.27 10.527-9.992 10.731-.115.005-.23.007-.346.007"/>
|
||||
<path d="M606.81 544.972v-.185a3.476 3.476 0 002.8-3.076 3.815 3.815 0 00-1.322-3.077 8.675 8.675 0 00-4.707-.984 27.066 27.066 0 00-5.2.431v13.538h3.077v-5.446h1.477c1.754 0 2.554.646 2.83 2.154a11.667 11.667 0 001.047 3.292h3.415a10.381 10.381 0 01-1.016-3.385 3.938 3.938 0 00-2.462-3.292m-3.723-.985h-1.508v-3.908a7.457 7.457 0 011.754 0c1.97 0 2.893.831 2.893 2.062 0 1.231-1.415 2-3.076 2m112.135-241.924c-1.938-1.539-20.03-15.199-58.181-15.199a185.562 185.562 0 00-30.061 2.584c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908-6.738 9.723a137.318 137.318 0 00-18.214 42.582c-6.831 28.891-2.677 56.027 11.999 79.226-17.722 9.876-46.151 12.307-51.904 12.522H30.096c-12.293.017-22.27 9.952-22.336 22.245a338.69 338.69 0 0020.614 120.946c16.214 42.521 40.336 73.842 71.719 93.01 35.167 21.537 92.302 33.844 157.067 33.844a468.88 468.88 0 0087.226-7.907 364.649 364.649 0 00113.839-41.352 312.969 312.969 0 0077.688-63.596c37.29-42.213 59.505-89.226 76.026-131.007h6.584c40.828 0 65.935-16.338 79.78-30.029a87.08 87.08 0 0021.045-31.167l2.923-8.553z"/>
|
||||
<path d="M73.725 337.54h63.073a5.507 5.507 0 005.507-5.508V275.85a5.507 5.507 0 00-5.476-5.538H73.725a5.507 5.507 0 00-5.507 5.507v56.212a5.507 5.507 0 005.507 5.508m86.918.001h63.073a5.507 5.507 0 005.507-5.508V275.85a5.507 5.507 0 00-5.477-5.538h-63.104a5.537 5.537 0 00-5.538 5.538v56.181a5.54 5.54 0 005.539 5.508m88.24.001h63.073a5.507 5.507 0 005.507-5.508V275.85a5.507 5.507 0 00-5.477-5.538h-63.104a5.507 5.507 0 00-5.507 5.507v56.212a5.51 5.51 0 005.508 5.508m87.195.001h63.073a5.537 5.537 0 005.538-5.508V275.85a5.537 5.537 0 00-5.538-5.538h-63.073a5.507 5.507 0 00-5.507 5.507v56.212a5.508 5.508 0 005.507 5.508m-175.435-80.826h63.073a5.539 5.539 0 005.507-5.538v-56.18a5.507 5.507 0 00-5.507-5.508h-63.073a5.538 5.538 0 00-5.538 5.507v56.181a5.568 5.568 0 005.538 5.538m88.24 0h63.073a5.539 5.539 0 005.507-5.538v-56.18a5.507 5.507 0 00-5.507-5.508h-63.073a5.507 5.507 0 00-5.507 5.507v56.181a5.539 5.539 0 005.507 5.538m87.195 0h63.073a5.568 5.568 0 005.538-5.538v-56.18a5.537 5.537 0 00-5.538-5.508h-63.073a5.507 5.507 0 00-5.507 5.507v56.181a5.539 5.539 0 005.507 5.538m0-80.856h63.073a5.538 5.538 0 005.538-5.507v-56.213a5.537 5.537 0 00-5.538-5.507h-63.073a5.506 5.506 0 00-5.507 5.507v56.212a5.508 5.508 0 005.507 5.508m87.964 161.683h63.073a5.537 5.537 0 005.538-5.508V275.85a5.537 5.537 0 00-5.538-5.538h-63.073a5.508 5.508 0 00-5.507 5.507v56.212a5.508 5.508 0 005.507 5.508"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.9 KiB |
157
src/index.html
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2023 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
|
||||
@@ -16,61 +16,102 @@
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- build:css docker-registry-ui.css -->
|
||||
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/css" />
|
||||
<link href="style.css" rel="stylesheet" type="text/css" />
|
||||
<link href="material-icons.css" rel="stylesheet" type="text/css" />
|
||||
<link href="roboto.css" rel="stylesheet" type="text/css" />
|
||||
<!-- endbuild -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:site_name" content="Docker Registry UI" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@Joxit" />
|
||||
<meta name="twitter:creator" content="@Jones Magloire" />
|
||||
<title>Docker Registry UI</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- build:css docker-registry-ui.css -->
|
||||
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="style.css" rel="stylesheet" type="text/css">
|
||||
<link href="material-icons.css" rel="stylesheet" type="text/css">
|
||||
<link href="roboto.css" rel="stylesheet" type="text/css">
|
||||
<!-- endbuild -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta property="og:site_name" content="Docker Registry UI" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@Joxit" />
|
||||
<meta name="twitter:creator" content="@Jones Magloire" />
|
||||
<title>Docker Registry UI</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- build:keep production -->
|
||||
<docker-registry-ui registry-url="${REGISTRY_URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
|
||||
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
|
||||
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}">
|
||||
</docker-registry-ui>
|
||||
<!-- endbuild -->
|
||||
<!-- build:keep developement -->
|
||||
<docker-registry-ui registry-url="" name="Developement Registry" pull-url="" show-content-digest="true"
|
||||
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false">
|
||||
</docker-registry-ui>
|
||||
<!-- endbuild -->
|
||||
<!-- build:js docker-registry-ui.js -->
|
||||
<script src="../node_modules/riot/riot+compiler.min.js"></script>
|
||||
<script src="../node_modules/riot-route/dist/route.js"></script>
|
||||
<script src="../node_modules/riot-mui/build/js/riot-mui.js"></script>
|
||||
<script src="tags/catalog.riot" type="riot/tag"></script>
|
||||
<script src="tags/catalog-element.riot" type="riot/tag"></script>
|
||||
<script src="tags/tag-history-button.riot" type="riot/tag"></script>
|
||||
<script src="tags/tag-history.riot" type="riot/tag"></script>
|
||||
<script src="tags/tag-history-element.riot" type="riot/tag"></script>
|
||||
<script src="tags/taglist.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-tag.riot" type="riot/tag"></script>
|
||||
<script src="tags/remove-image.riot" type="riot/tag"></script>
|
||||
<script src="tags/copy-to-clipboard.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/add.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/change.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/remove.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/menu.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-size.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-date.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-content-digest.riot" type="riot/tag"></script>
|
||||
<script src="tags/pagination.riot" type="riot/tag"></script>
|
||||
<script src="tags/app.riot" type="riot/tag"></script>
|
||||
<script src="scripts/http.js"></script>
|
||||
<script src="scripts/script.js"></script>
|
||||
<script src="scripts/utils.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<!-- build:keep production -->
|
||||
<docker-registry-ui
|
||||
registry-url="${REGISTRY_URL}"
|
||||
name="${REGISTRY_TITLE}"
|
||||
pull-url="${PULL_URL}"
|
||||
show-content-digest="${SHOW_CONTENT_DIGEST}"
|
||||
is-image-remove-activated="${DELETE_IMAGES}"
|
||||
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}"
|
||||
single-registry="${SINGLE_REGISTRY}"
|
||||
default-registries="${DEFAULT_REGISTRIES}"
|
||||
read-only-registries="${READ_ONLY_REGISTRIES}"
|
||||
show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"
|
||||
history-custom-labels="${HISTORY_CUSTOM_LABELS}"
|
||||
use-control-cache-header="${USE_CONTROL_CACHE_HEADER}"
|
||||
theme="${THEME}"
|
||||
theme-primary-text="${THEME_PRIMARY_TEXT}"
|
||||
theme-neutral-text="${THEME_NEUTRAL_TEXT}"
|
||||
theme-background="${THEME_BACKGROUND}"
|
||||
theme-hover-background="${THEME_HOVER_BACKGROUND}"
|
||||
theme-accent-text="${THEME_ACCENT_TEXT}"
|
||||
theme-header-text="${THEME_HEADER_TEXT}"
|
||||
theme-header-background="${THEME_HEADER_BACKGROUND}"
|
||||
theme-footer-text="${THEME_FOOTER_TEXT}"
|
||||
theme-footer-neutra-text="${THEME_FOOTER_NEUTRAL_TEXT}"
|
||||
theme-footer-background="${THEME_FOOTER_BACKGROUND}"
|
||||
>
|
||||
</docker-registry-ui>
|
||||
<!-- endbuild -->
|
||||
<!-- build:keep developement -->
|
||||
<docker-registry-ui
|
||||
registry-url=""
|
||||
name="Developement Registry"
|
||||
pull-url=""
|
||||
show-content-digest="true"
|
||||
is-image-remove-activated="true"
|
||||
catalog-elements-limit="1000"
|
||||
single-registry="false"
|
||||
show-catalog-nb-tags="true"
|
||||
history-custom-labels="first_custom_labels,second_custom_labels"
|
||||
use-control-cache-header="false"
|
||||
theme="auto"
|
||||
theme-primary-text=""
|
||||
theme-neutral-text=""
|
||||
theme-background=""
|
||||
theme-hover-background=""
|
||||
theme-accent-text=""
|
||||
theme-header-text=""
|
||||
theme-header-background=""
|
||||
theme-footer-text=""
|
||||
theme-footer-neutra-text=""
|
||||
theme-footer-background=""
|
||||
>
|
||||
</docker-registry-ui>
|
||||
<!-- endbuild -->
|
||||
<!-- build:js docker-registry-ui.js -->
|
||||
<script src="../node_modules/riot/riot+compiler.min.js"></script>
|
||||
<script src="../node_modules/riot-route/dist/route.js"></script>
|
||||
<script src="../node_modules/riot-mui/build/js/riot-mui.js"></script>
|
||||
<script src="tags/catalog.riot" type="riot/tag"></script>
|
||||
<script src="tags/catalog-element.riot" type="riot/tag"></script>
|
||||
<script src="tags/tag-history-button.riot" type="riot/tag"></script>
|
||||
<script src="tags/tag-history.riot" type="riot/tag"></script>
|
||||
<script src="tags/tag-history-element.riot" type="riot/tag"></script>
|
||||
<script src="tags/taglist.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-tag.riot" type="riot/tag"></script>
|
||||
<script src="tags/remove-image.riot" type="riot/tag"></script>
|
||||
<script src="tags/copy-to-clipboard.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/add.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/change.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/remove.riot" type="riot/tag"></script>
|
||||
<script src="tags/dialogs/menu.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-size.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-date.riot" type="riot/tag"></script>
|
||||
<script src="tags/image-content-digest.riot" type="riot/tag"></script>
|
||||
<script src="tags/pagination.riot" type="riot/tag"></script>
|
||||
<script src="tags/app.riot" type="riot/tag"></script>
|
||||
<script src="scripts/http.js"></script>
|
||||
<script src="scripts/script.js"></script>
|
||||
<script src="scripts/utils.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
MaterialCheckbox,
|
||||
MaterialTabs,
|
||||
MaterialSnackbar,
|
||||
MaterialDropdownList,
|
||||
MaterialDropdown,
|
||||
MaterialPopup,
|
||||
MaterialInput,
|
||||
MaterialSwitch,
|
||||
} from 'riot-mui';
|
||||
|
||||
import DockerRegistryUI from './components/docker-registry-ui.riot';
|
||||
@@ -28,9 +29,10 @@ register('material-waves', MaterialWaves);
|
||||
register('material-checkbox', MaterialCheckbox);
|
||||
register('material-snackbar', MaterialSnackbar);
|
||||
register('material-tabs', MaterialTabs);
|
||||
register('material-dropdown-list', MaterialDropdownList);
|
||||
register('material-dropdown', MaterialDropdown);
|
||||
register('material-popup', MaterialPopup);
|
||||
register('material-input', MaterialInput);
|
||||
register('material-switch', MaterialSwitch);
|
||||
|
||||
const createApp = component(DockerRegistryUI);
|
||||
const tags = document.getElementsByTagName('docker-registry-ui');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
* Copyright (C) 2016-2023 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
|
||||
@@ -46,7 +46,7 @@ export function compare(e1, e2) {
|
||||
}
|
||||
|
||||
export class DockerImage {
|
||||
constructor(name, tag, { list, registryUrl, onNotify, onAuthentication }) {
|
||||
constructor(name, tag, { list, registryUrl, onNotify, onAuthentication, useControlCacheHeader }) {
|
||||
this.name = name;
|
||||
this.tag = tag;
|
||||
this.chars = 0;
|
||||
@@ -55,7 +55,9 @@ export class DockerImage {
|
||||
registryUrl,
|
||||
onNotify,
|
||||
onAuthentication,
|
||||
useControlCacheHeader,
|
||||
};
|
||||
this.ociImage = false;
|
||||
observable(this);
|
||||
this.on('get-size', function () {
|
||||
if (this.size !== undefined) {
|
||||
@@ -82,8 +84,8 @@ export class DockerImage {
|
||||
return this.trigger('content-digest-chars', this.chars);
|
||||
});
|
||||
this.on('get-content-digest', function () {
|
||||
if (this.digest !== undefined) {
|
||||
return this.trigger('content-digest', this.digest);
|
||||
if (this.contentDigest !== undefined) {
|
||||
return this.trigger('content-digest', this.contentDigest);
|
||||
}
|
||||
return this.fillInfo();
|
||||
});
|
||||
@@ -96,7 +98,7 @@ export class DockerImage {
|
||||
const oReq = new Http({ onAuthentication: this.opts.onAuthentication });
|
||||
const self = this;
|
||||
oReq.addEventListener('loadend', function () {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
if (this.status === 200 || this.status === 202) {
|
||||
const response = JSON.parse(this.responseText);
|
||||
if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json' && self.opts.list) {
|
||||
self.trigger('list', response);
|
||||
@@ -107,22 +109,30 @@ export class DockerImage {
|
||||
self.variants = [image];
|
||||
return;
|
||||
}
|
||||
self.size = response.layers.reduce(function (acc, e) {
|
||||
self.ociImage = response.mediaType === 'application/vnd.oci.image.index.v1+json';
|
||||
self.layers = response.layers || response.manifests;
|
||||
self.size = self.layers.reduce(function (acc, e) {
|
||||
return acc + e.size;
|
||||
}, 0);
|
||||
self.sha256 = response.config.digest;
|
||||
self.layers = response.layers;
|
||||
self.sha256 = response.config && response.config.digest;
|
||||
self.trigger('size', self.size);
|
||||
self.trigger('sha256', self.sha256);
|
||||
oReq.getContentDigest(function (digest) {
|
||||
self.digest = digest;
|
||||
self.trigger('content-digest', digest);
|
||||
if (!digest) {
|
||||
oReq.getContentDigest(function (contentDigest) {
|
||||
self.contentDigest = contentDigest;
|
||||
self.trigger('content-digest', contentDigest);
|
||||
if (!contentDigest) {
|
||||
self.opts.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
|
||||
}
|
||||
});
|
||||
self.getBlobs(response.config.digest);
|
||||
} else if (this.status == 404) {
|
||||
if (!self.ociImage) {
|
||||
self.getBlobs(self.sha256);
|
||||
} else {
|
||||
// Force updates
|
||||
self.trigger('creation-date');
|
||||
self.trigger('blobs');
|
||||
self.trigger('oci-image');
|
||||
}
|
||||
} else if (this.status === 404) {
|
||||
self.opts.onNotify(`Manifest for ${self.name}:${self.tag} not found`, true);
|
||||
} else {
|
||||
self.opts.onNotify(this.responseText);
|
||||
@@ -131,16 +141,19 @@ export class DockerImage {
|
||||
oReq.open('GET', `${this.opts.registryUrl}/v2/${self.name}/manifests/${self.tag}`);
|
||||
oReq.setRequestHeader(
|
||||
'Accept',
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' +
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json' +
|
||||
(self.opts.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : '')
|
||||
);
|
||||
if (self.opts.useControlCacheHeader) {
|
||||
oReq.setRequestHeader('Cache-Control', 'no-store, no-cache');
|
||||
}
|
||||
oReq.send();
|
||||
}
|
||||
getBlobs(blob) {
|
||||
const oReq = new Http({ onAuthentication: this.opts.onAuthentication });
|
||||
const self = this;
|
||||
oReq.addEventListener('loadend', function () {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
if (this.status === 200 || this.status === 202) {
|
||||
const response = JSON.parse(this.responseText);
|
||||
self.creationDate = new Date(response.created);
|
||||
self.blobs = response;
|
||||
@@ -155,8 +168,13 @@ export class DockerImage {
|
||||
self.blobs.id = blob.replace('sha256:', '');
|
||||
self.trigger('creation-date', self.creationDate);
|
||||
self.trigger('blobs', self.blobs);
|
||||
} else if (this.status == 404) {
|
||||
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found`, true);
|
||||
} else if (this.status === 404) {
|
||||
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found: blob '${self.blobs}'`, true);
|
||||
} else if (!this.responseText) {
|
||||
self.opts.onNotify(
|
||||
`Can"t get blobs for ${self.name}:${self.tag}: blob '${self.blobs}' (no message error)`,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
self.opts.onNotify(this.responseText);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
* Copyright (C) 2016-2023 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
|
||||
@@ -52,8 +52,9 @@ export class Http {
|
||||
switch (e) {
|
||||
case 'loadend': {
|
||||
self.oReq.addEventListener('loadend', function () {
|
||||
if (this.status == 401 && !this.withCredentials) {
|
||||
const tokenAuth = parseAuthenticateHeader(this.getResponseHeader('www-authenticate'));
|
||||
if (this.status === 401 && !this.withCredentials) {
|
||||
const tokenAuth =
|
||||
this.hasHeader('www-authenticate') && parseAuthenticateHeader(this.getResponseHeader('www-authenticate'));
|
||||
self.onAuthentication(tokenAuth, (bearer) => {
|
||||
const req = new XMLHttpRequest();
|
||||
req._url = self._url;
|
||||
@@ -65,12 +66,12 @@ export class Http {
|
||||
req.setRequestHeader(key, self._headers[key]);
|
||||
}
|
||||
if (bearer && bearer.token) {
|
||||
req.setRequestHeader('Authorization', `Bearer ${bearer.token}`)
|
||||
req.setRequestHeader('Authorization', `Bearer ${bearer.token}`);
|
||||
} else {
|
||||
req.withCredentials = true;
|
||||
}
|
||||
req.hasHeader = hasHeader;
|
||||
req.getErrorMessage = Http.getErrorMessage;
|
||||
req.getErrorMessage = getErrorMessage;
|
||||
self.oReq = req;
|
||||
req.send();
|
||||
});
|
||||
@@ -127,15 +128,9 @@ const hasHeader = function (header) {
|
||||
|
||||
const getErrorMessage = function () {
|
||||
if (this._url.match('^http://') && window.location.protocol === 'https:') {
|
||||
return (
|
||||
'Mixed Content: The page at `' +
|
||||
window.location.origin +
|
||||
'` was loaded over HTTPS, but requested an insecure server endpoint `' +
|
||||
new URL(this._url).origin +
|
||||
'`. This request has been blocked; the content must be served over HTTPS.'
|
||||
);
|
||||
return { code: 'MIXED_CONTENT', url: this._url };
|
||||
} else if (!this._url || !this._url.match('^http')) {
|
||||
return 'Incorrect server endpoint.';
|
||||
return { code: 'INCORRECT_URL', url: this._url };
|
||||
} else if (this.withCredentials && !this.hasHeader('Access-Control-Allow-Credentials')) {
|
||||
return (
|
||||
"The `Access-Control-Allow-Credentials` header in the response is missing and must be set to `true` when the request's credentials mode is on. Origin `" +
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
* Copyright (C) 2016-2023 Jones Magloire @Joxit
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -14,7 +14,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/>.
|
||||
*/
|
||||
import { router, getCurrentRoute } from '@riotjs/route';
|
||||
import { getCurrentRoute } from '@riotjs/route';
|
||||
import { encodeURI, decodeURI } from './utils';
|
||||
|
||||
function getQueryParams() {
|
||||
@@ -59,16 +59,16 @@ function baseUrl(qs) {
|
||||
|
||||
export default {
|
||||
home() {
|
||||
router.push(baseUrl({ page: null }));
|
||||
return baseUrl({ page: null });
|
||||
},
|
||||
taglist(image) {
|
||||
router.push(`${baseUrl({ page: null })}#!/taglist/${image}`);
|
||||
return `${baseUrl({ page: null })}#!/taglist/${image}`;
|
||||
},
|
||||
getTagListImage() {
|
||||
return getCurrentRoute().replace(/^.*(#!)?\/?taglist\//, '');
|
||||
},
|
||||
history(image, tag) {
|
||||
router.push(`${baseUrl({ page: null })}#!/taghistory/image/${image}/tag/${tag}`);
|
||||
return `${baseUrl({ page: null })}#!/taghistory/image/${image}/tag/${tag}`;
|
||||
},
|
||||
getTagHistoryImage() {
|
||||
return getCurrentRoute().replace(/^.*(#!)?\/?taghistory\/image\/(.*)\/tag\/(.*)\/?$/, '$2');
|
||||
|
||||
64
src/scripts/theme.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const LIGHT_THEME = {
|
||||
'primary-text': '#25313b',
|
||||
'neutral-text': '#777',
|
||||
'background': '#fff',
|
||||
'hover-background': '#eee',
|
||||
'accent-text': '#6680a1',
|
||||
'header-text': '#fff',
|
||||
'header-background': '#25313b',
|
||||
'footer-text': '#fff',
|
||||
'footer-neutral-text': '#999',
|
||||
'footer-background': '#555',
|
||||
};
|
||||
const DARK_THEME = {
|
||||
'primary-text': '#8A9EBA',
|
||||
'neutral-text': '#36527A',
|
||||
'background': '#22272e',
|
||||
'hover-background': '#30404D',
|
||||
'accent-text': '#5684FF',
|
||||
'header-text': '#fff',
|
||||
'header-background': '#333A45',
|
||||
'footer-text': '#fff',
|
||||
'footer-neutral-text': '#999',
|
||||
'footer-background': '#555',
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_THEME = 'registryUiTheme';
|
||||
|
||||
let THEME;
|
||||
|
||||
const normalizeKey = (k) =>
|
||||
k
|
||||
.replace(/([A-Z])/g, '-$1')
|
||||
.toLowerCase()
|
||||
.replace(/^theme-/, '');
|
||||
|
||||
const preferDarkMode = ({ theme }) => {
|
||||
if (theme === 'auto' || theme === "") {
|
||||
switch (localStorage.getItem(LOCAL_STORAGE_THEME)) {
|
||||
case 'dark':
|
||||
return true;
|
||||
case 'light':
|
||||
return false;
|
||||
default:
|
||||
if (typeof window.matchMedia === 'function') {
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
return prefersDarkScheme && prefersDarkScheme.matches;
|
||||
}
|
||||
}
|
||||
}
|
||||
return theme === 'dark';
|
||||
};
|
||||
|
||||
export const loadTheme = (props, style) => {
|
||||
const isDarkMode = preferDarkMode(props);
|
||||
THEME = isDarkMode ? { ...DARK_THEME } : { ...LIGHT_THEME };
|
||||
Object.entries(props)
|
||||
.filter(([k, v]) => v && /^theme[A-Z]/.test(k))
|
||||
.map(([k, v]) => [normalizeKey(k), v])
|
||||
.forEach(([k, v]) => (THEME[k] = v));
|
||||
Object.entries(THEME).forEach(([k, v]) => style.setProperty(`--${k}`, v));
|
||||
const theme = isDarkMode ? 'dark' : 'light';
|
||||
localStorage.setItem(LOCAL_STORAGE_THEME, theme);
|
||||
return theme;
|
||||
};
|
||||
@@ -1,12 +1,19 @@
|
||||
const LOCAL_STORAGE_KEY = 'registryServer';
|
||||
|
||||
export function bytesToSize(bytes) {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes == undefined || isNaN(bytes)) {
|
||||
return '?';
|
||||
} else if (bytes == 0) {
|
||||
} else if (bytes === 0) {
|
||||
return '0 Byte';
|
||||
}
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
return Math.ceil(bytes / Math.pow(1024, i)) + ' ' + sizes[i];
|
||||
const number = bytes / Math.pow(1024, i);
|
||||
if (number < 10) {
|
||||
const decimal = (bytes - Math.floor(number) * Math.pow(1024, i)) / Math.pow(1024, i);
|
||||
return `${Math.floor(number)}.${Math.floor(decimal * 10)} ${sizes[i]}`;
|
||||
}
|
||||
return Math.ceil(number) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function dateFormat(date) {
|
||||
@@ -73,8 +80,13 @@ export function getHistoryIcon(attribute) {
|
||||
return 'get_app';
|
||||
case 'ExposedPorts':
|
||||
return 'router';
|
||||
case 'comment':
|
||||
return 'chat';
|
||||
default:
|
||||
'';
|
||||
if (attribute.startsWith('custom-label-')) {
|
||||
return 'label';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +143,13 @@ export function stripHttps(url) {
|
||||
return url.replace(/^https?:\/\//, '');
|
||||
}
|
||||
|
||||
function kebabToCamelCase(s) {
|
||||
return s.replace(/-[a-z]/, (x) => x[1].toUpperCase());
|
||||
}
|
||||
|
||||
export function eventTransfer(from, to) {
|
||||
from.on('*', function (event, param) {
|
||||
to[kebabToCamelCase(event)] = param;
|
||||
to.trigger(event, param);
|
||||
});
|
||||
}
|
||||
@@ -152,7 +169,7 @@ export const ERROR_CAN_NOT_READ_CONTENT_DIGEST = {
|
||||
|
||||
export function getRegistryServers(i) {
|
||||
try {
|
||||
const res = JSON.parse(localStorage.getItem('registryServer'));
|
||||
const res = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
|
||||
if (res instanceof Array) {
|
||||
return !isNaN(i) ? res[i] : res.map((url) => url.trim().replace(/\/*$/, ''));
|
||||
}
|
||||
@@ -160,6 +177,28 @@ export function getRegistryServers(i) {
|
||||
return !isNaN(i) ? '' : [];
|
||||
}
|
||||
|
||||
export function setRegistryServers(registries) {
|
||||
if (typeof registries === 'string') {
|
||||
registries = registries.split(',');
|
||||
} else if (!Array.isArray(registries)) {
|
||||
throw new Error('setRegistries must be called with string or array parameter');
|
||||
}
|
||||
registries = registries.map((registry) => registry.replace(/\/*$/, ''));
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(registries));
|
||||
}
|
||||
|
||||
export function addRegistryServers(registry) {
|
||||
const url = registry.trim().replace(/\/*$/, '');
|
||||
const registryServer = getRegistryServers().filter((e) => e !== url);
|
||||
setRegistryServers([url].concat(registryServer));
|
||||
return url;
|
||||
}
|
||||
|
||||
export function removeRegistryServers(registry) {
|
||||
const registryServers = getRegistryServers().filter((e) => e !== registry);
|
||||
setRegistryServers(registryServers);
|
||||
}
|
||||
|
||||
export function encodeURI(url) {
|
||||
if (!url) {
|
||||
return;
|
||||
@@ -175,5 +214,9 @@ export function decodeURI(url) {
|
||||
}
|
||||
|
||||
export function truthy(value) {
|
||||
return value === true || value === "true";
|
||||
}
|
||||
return value === true || value === 'true';
|
||||
}
|
||||
|
||||
export function stringToArray(value) {
|
||||
return value && typeof value === 'string' ? value.split(',') : [];
|
||||
}
|
||||
|
||||
206
src/style.scss
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Jones Magloire @Joxit
|
||||
* Copyright (C) 2016-2023 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
|
||||
@@ -23,18 +23,23 @@
|
||||
@import 'riot-mui/src/material-elements/material-checkbox/material-checkbox.scss';
|
||||
@import 'riot-mui/src/material-elements/material-tabs/material-tabs.scss';
|
||||
@import 'riot-mui/src/material-elements/material-snackbar/material-snackbar.scss';
|
||||
@import 'riot-mui/src/material-elements/material-dropdown-list/material-dropdown-list.scss';
|
||||
@import 'riot-mui/src/material-elements/material-dropdown/material-dropdown.scss';
|
||||
@import 'riot-mui/src/material-elements/material-popup/material-popup.scss';
|
||||
@import 'riot-mui/src/material-elements/material-input/material-input.scss';
|
||||
@import 'riot-mui/src/material-elements/material-switch/material-switch.scss';
|
||||
|
||||
@import './roboto.scss';
|
||||
@import './material-icons.scss';
|
||||
|
||||
|
||||
html > body {
|
||||
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif !important;
|
||||
font-size: 16px;
|
||||
background-color: var(--background);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -58,55 +63,41 @@ html, body {
|
||||
text-decoration: none;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
material-card, material-tabs, pagination .conatianer {
|
||||
material-card {
|
||||
background-color: var(--background);
|
||||
}
|
||||
material-card,
|
||||
material-tabs,
|
||||
pagination .container {
|
||||
max-width: 95%;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
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 {
|
||||
@media screen and (min-width: 1515px) {
|
||||
material-card,
|
||||
material-tabs,
|
||||
pagination .container {
|
||||
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);
|
||||
-webkit-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
-ms-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
-moz-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
-o-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 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;
|
||||
}
|
||||
@@ -117,23 +108,21 @@ material-spinner {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
material-navbar {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
material-navbar nav-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding: 0 16px 0 72px;
|
||||
text-decoration: none;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
letter-spacing: .02em;
|
||||
letter-spacing: 0.02em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.logo {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding: 16px;
|
||||
margin: auto;
|
||||
@@ -156,7 +145,7 @@ h2 {
|
||||
}
|
||||
|
||||
.list.highlight:hover {
|
||||
background-color: #eee;
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -169,18 +158,22 @@ h2 {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
docker-registry-ui material-button > :first-child .content i.material-icons,
|
||||
docker-registry-ui material-button > :first-child .content i.material-icons.material-icons {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.list > span i.material-icons,
|
||||
.list > li i.material-icons {
|
||||
margin-right: 32px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px;
|
||||
box-sizing: border-box;
|
||||
color: #757575;
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
.list > span .right i.material-icons.animated {
|
||||
transition: all 350ms cubic-bezier(.4,0,.2,1);
|
||||
transition: all 350ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@@ -237,11 +230,10 @@ material-card table {
|
||||
width: 100%;
|
||||
border: none;
|
||||
position: relative;
|
||||
border: 1px solid rgba(0, 0, 0, .12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-collapse: collapse;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -250,7 +242,7 @@ material-card table th {
|
||||
vertical-align: bottom;
|
||||
line-height: 24px;
|
||||
height: 48px;
|
||||
color: rgba(0, 0, 0, .54);
|
||||
color: var(--accent-text);
|
||||
box-sizing: border-box;
|
||||
padding: 0 18px 12px 18px;
|
||||
text-align: right;
|
||||
@@ -260,17 +252,15 @@ material-card table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
material-card material-button:hover,
|
||||
material-card table tbody tr:hover,
|
||||
pagination material-button:hover {
|
||||
background-color: #eee;
|
||||
material-card .material-card-title-action material-button:hover button,
|
||||
material-card table tbody tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
material-card material-button,
|
||||
material-card table tbody tr,
|
||||
pagination material-button {
|
||||
transition-duration: .28s;
|
||||
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
|
||||
material-button > :first-child[inverted='true'],
|
||||
material-card table tbody tr {
|
||||
transition-duration: 0.28s;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-property: background-color;
|
||||
}
|
||||
|
||||
@@ -283,8 +273,8 @@ material-card table td {
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
height: 48px;
|
||||
border-top: 1px solid rgba(0, 0, 0, .12);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .12);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 12px 18px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
@@ -292,32 +282,35 @@ material-card table td {
|
||||
}
|
||||
|
||||
tag-history-button button:hover,
|
||||
material-card table th.material-card-th-sorted-ascending:hover, material-card table th.material-card-th-sorted-descending:hover {
|
||||
material-card table th.material-card-th-sorted-ascending:hover,
|
||||
material-card table th.material-card-th-sorted-descending:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
material-card table th.material-card-th-sorted-ascending:hover:before, material-card table th.material-card-th-sorted-descending:hover:before {
|
||||
color: rgba(0, 0, 0, .26);
|
||||
material-card table th.material-card-th-sorted-ascending:hover:before,
|
||||
material-card table th.material-card-th-sorted-descending:hover:before {
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
material-card table th.material-card-th-sorted-ascending:before, material-card table th.material-card-th-sorted-descending:before {
|
||||
material-card table th.material-card-th-sorted-ascending:before,
|
||||
material-card table th.material-card-th-sorted-descending:before {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
font-size: 16px;
|
||||
content: "\e5d8";
|
||||
content: '\e5d8';
|
||||
margin-right: 5px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
material-card table th.material-card-th-sorted-descending:before {
|
||||
content: "\e5db";
|
||||
content: '\e5db';
|
||||
}
|
||||
|
||||
material-button .content i.material-icons,
|
||||
.material-icons {
|
||||
color: #777;
|
||||
color: var(--neutral-text);
|
||||
}
|
||||
|
||||
material-button[disabled] .content i.material-icons,
|
||||
@@ -329,19 +322,11 @@ material-snackbar .toast {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
material-popup material-button,
|
||||
pagination material-button {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
material-popup material-button:hover material-waves {
|
||||
background-color: hsla(0, 0%, 75%, .2);
|
||||
}
|
||||
|
||||
material-popup .popup {
|
||||
material-popup .popup > .content {
|
||||
padding: 1em;
|
||||
max-width: 450px;
|
||||
top: 2em;
|
||||
background-color: var(--background);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
footer {
|
||||
@@ -356,6 +341,9 @@ main {
|
||||
|
||||
material-footer {
|
||||
padding: 0.5em 1em;
|
||||
li {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-to-clipboard {
|
||||
@@ -366,7 +354,8 @@ material-footer {
|
||||
/* 5 + 2 + 3 + 24 + 3 + 2 + 18 */
|
||||
padding-right: 57px;
|
||||
}
|
||||
image-tag, .copy-to-clipboard {
|
||||
image-tag,
|
||||
.copy-to-clipboard {
|
||||
display: inline-block;
|
||||
}
|
||||
image-content-digest {
|
||||
@@ -405,28 +394,7 @@ taglist .image-size {
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
catalog material-card,
|
||||
tag-history material-card {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
tag-history-button button {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
material-card material-button,
|
||||
pagination material-button {
|
||||
max-height: 30px;
|
||||
max-width: 30px;
|
||||
}
|
||||
|
||||
material-button:hover material-waves {
|
||||
background: none;
|
||||
}
|
||||
|
||||
material-card material-button,
|
||||
pagination material-button {
|
||||
material-card material-button {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
@@ -436,7 +404,7 @@ catalog-element material-card {
|
||||
}
|
||||
|
||||
catalog-element catalog-element material-card {
|
||||
transition: all 350ms cubic-bezier(.4,0,.2,1);
|
||||
transition: all 350ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
@@ -451,7 +419,7 @@ catalog-element catalog-element > .content {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1515px){
|
||||
@media screen and (min-width: 1515px) {
|
||||
catalog-element catalog-element > .content material-card {
|
||||
max-width: calc(1440px - 3em);
|
||||
}
|
||||
@@ -483,35 +451,9 @@ material-checkbox.indeterminate .checkbox.checked .checkmark {
|
||||
}
|
||||
|
||||
material-checkbox .checkbox {
|
||||
border-color: #777;
|
||||
border-color: var(--neutral-text);
|
||||
}
|
||||
|
||||
material-checkbox .checkbox.checked {
|
||||
background-color: #777;
|
||||
background-color: var(--neutral-text);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||