diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 73b1044..25866ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Standard Version + - name: Build and push Beta Version uses: docker/build-push-action@v2 with: context: . @@ -31,14 +31,4 @@ jobs: push: true tags: | joxit/docker-registry-ui:master - joxit/docker-registry-ui:main - - name: Build and push Static Version - uses: docker/build-push-action@v2 - with: - context: . - file: ./static.dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x - push: true - tags: | - joxit/docker-registry-ui:master-static - joxit/docker-registry-ui:main-static \ No newline at end of file + joxit/docker-registry-ui:main \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32d2bb6..9db62ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Standard Version + - name: Build and push Latest Version uses: docker/build-push-action@v2 with: context: . @@ -51,14 +51,4 @@ jobs: push: true tags: | joxit/docker-registry-ui:latest - joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}} - - name: Build and push Static version - uses: docker/build-push-action@v2 - with: - context: . - file: ./static.dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x - push: true - tags: | - joxit/docker-registry-ui:static - joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}-static \ No newline at end of file + joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 53aa796..f4cc2db 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ registry-data .idea _site *.orig +.serve/ diff --git a/Dockerfile b/Dockerfile index b220f5f..0282147 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit" WORKDIR /usr/share/nginx/html/ +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh COPY dist/ /usr/share/nginx/html/ COPY favicon.ico /usr/share/nginx/html/ \ No newline at end of file diff --git a/Dockerfile.static b/Dockerfile.static deleted file mode 120000 index ae6a841..0000000 --- a/Dockerfile.static +++ /dev/null @@ -1 +0,0 @@ -static.dockerfile \ No newline at end of file diff --git a/README.md b/README.md index cc44023..85e33c9 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,11 @@ title: Docker Registry User Interface ## Overview -This project aims to provide a simple and complete user interface for your private docker registry. -You have the choice between two versions, the **standard interface** (`joxit/docker-registry-ui:latest`) and the **static interface** (`joxit/docker-registry-ui:static`). +:warning: `joxit/docker-registry-ui:master` and `joxit/docker-registry-ui:main` tags contains unreleased v2.0.0 version! -In the **standard interface**, there is no default registry, you need to add your own within the UI. -With this version, you can manage **more than one** registry server but all the environment variables will be **unavailable**. -All registries will be stored in the [local storage](https://en.wikipedia.org/wiki/Web_storage#Local_and_session_storage) of your browser. No configuration is needed when you launch the UI. +This project aims to provide a simple and complete user interface for your private docker registry. You can customize the interface with various options. The major option is `SINGLE_REGISTRY` which allows you to disable the dynamic selection of docker registeries (same behavior as the old **static** tag). -In the **static interface**, it will connect to a single registry and will not change. The configuration is done at the start of the interface, when you use the docker images whose tags contain the `static` keyword. With this version, you can manage **only one registry** and all environment variables will be **available**. +You may need the [migration guide from 1.x to 2.x](https://github.com/Joxit/docker-registry-ui/wiki/Migrating-from-1.x-to-2.x) or [the 1.x readme](https://github.com/Joxit/docker-registry-ui/blob/8fe3adf12540d1316cb57628ebe86a392a703d90/README.md) This web user interface uses [Riot](https://github.com/Riot/riot) the react-like user interface micro-library and [riot-mui](https://github.com/kysonic/riot-mui) components. @@ -40,20 +37,20 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like - Display image/tag count (see [#56 issue comment](https://github.com/Joxit/docker-registry-ui/issues/56#issuecomment-449246524)). - Select multiple tags to delete (see [#29](https://github.com/Joxit/docker-registry-ui/issues/29)). - Select all tags with ALT + Click to delete (see [#80](https://github.com/Joxit/docker-registry-ui/issues/80)). -- One interface for many registries **standard interface**. -- Share your docker registry with query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`) **standard interface**. -- Use `joxit/docker-registry-ui:static` as reverse proxy (with `REGISTRY_URL` environment variable) to your docker registry (This will avoid CORS) **static interface**. -- Add Title when using `REGISTRY_URL` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)) **static interface**. -- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)) **static interface**. -- Add custom header via environment variable and file via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) **static interface** -- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)) **static interface**. -- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)) **static interface**. +- One interface for many registries (when `SINGLE_REGISTRY=false`). +- Share your docker registry with query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`) (when `SINGLE_REGISTRY=false`). +- Use the UI as reverse proxy (with `REGISTRY_URL` environment variable) to your docker registry (This will avoid CORS). +- Add Title when using `REGISTRY_URL` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)). +- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)). +- Add custom header via environment variable and file via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) +- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)). +- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)). - Multi arch support in history page (see [#130](https://github.com/Joxit/docker-registry-ui/issues/130) and [#134](https://github.com/Joxit/docker-registry-ui/pull/134)) ## FAQ -- What is the difference between **`joxit/docker-registry-ui:latest`** and **`joxit/docker-registry-ui:static`** tags ? - - The `latest` tag was the first version of the project, one UI for many docker registries. The `static` tag allows you to have an interface for a single registry and also allows you select your features. +- What is the difference between **`SINGLE_REGISTRY=false`** and **`SINGLE_REGISTRY=true`** options ? + - When `SINGLE_REGISTRY` is set to false, a menu appears on the interface allowing you to dynamically change docker registry URLs. - Why, when I delete all tags of an image, the image is still in the UI ? - This is a limitation of docker registry, the garbage collector don't remove empty images. If you want to delete dangling images, you will need to delete the folder in your registry data. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77)) - Why the image size in the UI is not the same as displayed during `docker images` ? @@ -80,46 +77,19 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples) or open an issue. -## Getting Started - -The docker image contains the source code and nginx in order to serve the docker-registry-ui. Please remember the difference between the **standard interface** (`latest` tag) and **static interface** (`static` tags). - -### Run the standard interface - -You can run the standard interface see the website on your 80 port. You will be able to use the interface for **many registry servers**, but all the configuration via environment variables from the static interface will be **unavailable**. - -```sh -docker run -d -p 80:80 joxit/docker-registry-ui:latest -``` - -### Run the static interface +## Available options Some env options are available for use this interface for **only one server**. -- [`URL`](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone): set the static URL to use (You will need CORS configuration). Example: `http://127.0.0.1:5000`. (`Required`) -- [`REGISTRY_URL`](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy): your docker registry URL to contact (CORS configuration is not needed). Example: `http://my-docker-container:5000`. (Can't be used with `URL`, since 0.3.2). -- `DELETE_IMAGES`: if this variable is empty or `false`, delete feature is deactivated. It is activated otherwise. -- `REGISTRY_TITLE`: Set a custom title for your user interface when using `REGISTRY_URL` (since 0.3.4). -- `PULL_URL`: Set a custom url for the docker pull command, this is useful when you use `REGISTRY_URL` and your registry is on a different host (since 1.1.0). -- [`NGINX_PROXY_HEADER_*`](https://github.com/Joxit/docker-registry-ui/tree/main/examples/proxy-headers): Set custom headers for your docker registry, usefull when you want to add your credentials. (Can be use only with `REGISTRY_URL`). -- [`SHOW_CONTENT_DIGEST`](https://github.com/Joxit/docker-registry-ui/issues/126): Show content digest in docker tag list. Default: `true`. -- [`CATALOG_ELEMENTS_LIMIT`](https://github.com/Joxit/docker-registry-ui/pull/132): Limit the number of elements in the catalog page. Default: `100000`. - -Example with `URL` option. - -```sh -docker run -d -p 80:80 -e URL=http://127.0.0.1:5000 -e DELETE_IMAGES=true joxit/docker-registry-ui:static -``` - -Example with `REGISTRY_URL`, this will add a proxy to your registry. -Your registry will be accessible here : `http://127.0.0.1/v2`, this will avoid CORS errors (see [#25](https://github.com/Joxit/docker-registry-ui/issues/25#issuecomment-360522487)). -Be careful, `joxit/docker-registry-ui` and `registry:2` will communicate, both containers should be in the same network or use your private IP. - -```sh -docker network create registry-ui-net -docker run -d --net registry-ui-net --name registry-srv registry:2 -docker run -d --net registry-ui-net -p 80:80 -e REGISTRY_URL=http://registry-srv:5000 -e DELETE_IMAGES=true -e REGISTRY_TITLE="My registry" joxit/docker-registry-ui:static -``` +- `REGISTRY_URL`: The default url of your docker registry. You may need CORS configuration on your registry. (default: derived from the hostname of your UI). +- `REGISTRY_TITLE`: Set a custom title for your user interface. (default: value derived from `REGISTRY_URL`). +- `PULL_URL`: Set a custom url when you copy the `docker pull` command. (default: value derived from `REGISTRY_URL`). +- `DELETE_IMAGES`: Set if we can delete images from the UI. (default: `false`) +- `SHOW_CONTENT_DIGEST`: Show content digest in docker tag list. (default: `true`) +- `CATALOG_ELEMENTS_LIMIT`: Limit the number of elements in the catalog page. (default: `100000`). +- `SINGLE_REGISTRY`: Remove the menu that show the dialogs to add, remove and change the endpoint of your docker registry. (default `false`) +- `NGINX_PROXY_PASS_URL`: Update the default Nginx configuration and set the **proxy_pass** to your backend docker registry (this avoid CORS configuration). +- `NGINX_PROXY_HEADER_*`: Update the default Nginx configuration and set **custom headers** for your backend docker registry. Only when `NGINX_PROXY_PASS_URL` is used. There are some examples with [docker-compose](https://docs.docker.com/compose/) and docker-registry-ui as proxy [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy/) or docker-registry-ui as standalone [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone/). @@ -185,7 +155,7 @@ http: addr: :5000 headers: X-Content-Type-Options: [nosniff] - Access-Control-Allow-Origin: ['http://127.0.0.1:8001'] + Access-Control-Allow-Origin: ['http://127.0.0.1:8000'] Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] Access-Control-Allow-Headers: ['Authorization', 'Accept'] Access-Control-Max-Age: [1728000] diff --git a/arm32v7-static.dockerfile b/arm32v7-static.dockerfile deleted file mode 100644 index c6b52ba..0000000 --- a/arm32v7-static.dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2016-2018 Jones Magloire @Joxit -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -FROM arm32v7/nginx - -LABEL maintainer="Jones MAGLOIRE @Joxit" - -WORKDIR /usr/share/nginx/html/ - -ENV NGINX_PROXY_HEADER_Host '$http_host' - -COPY nginx/default.conf /etc/nginx/conf.d/default.conf -COPY dist/ /usr/share/nginx/html/ -COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js -COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh -COPY favicon.ico /usr/share/nginx/html/ diff --git a/arm32v7.dockerfile b/arm32v7.dockerfile index cdde137..ca3ee39 100644 --- a/arm32v7.dockerfile +++ b/arm32v7.dockerfile @@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit" WORKDIR /usr/share/nginx/html/ +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh COPY dist/ /usr/share/nginx/html/ COPY favicon.ico /usr/share/nginx/html/ diff --git a/arm64v8-static.dockerfile b/arm64v8-static.dockerfile deleted file mode 100644 index 41ef7a4..0000000 --- a/arm64v8-static.dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2016-2018 Jones Magloire @Joxit -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -FROM arm64v8/nginx - -LABEL maintainer="Jones MAGLOIRE @Joxit" - -WORKDIR /usr/share/nginx/html/ - -ENV NGINX_PROXY_HEADER_Host '$http_host' - -COPY nginx/default.conf /etc/nginx/conf.d/default.conf -COPY dist/ /usr/share/nginx/html/ -COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js -COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh -COPY favicon.ico /usr/share/nginx/html/ diff --git a/arm64v8.dockerfile b/arm64v8.dockerfile index 31d9b31..8956830 100644 --- a/arm64v8.dockerfile +++ b/arm64v8.dockerfile @@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit" WORKDIR /usr/share/nginx/html/ +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh COPY dist/ /usr/share/nginx/html/ COPY favicon.ico /usr/share/nginx/html/ diff --git a/bin/entrypoint b/bin/entrypoint index 80377ec..eb34220 100755 --- a/bin/entrypoint +++ b/bin/entrypoint @@ -1,19 +1,20 @@ #!/bin/sh -sed -i "s,\${URL},${URL}," scripts/docker-registry-ui.js -sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," scripts/docker-registry-ui.js -sed -i "s,\${PULL_URL},${PULL_URL}," scripts/docker-registry-ui.js +sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," index.html +sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," index.html +sed -i "s,\${PULL_URL},${PULL_URL}," index.html +sed -i "s,\${SINGLE_REGISTRY},${SINGLE_REGISTRY}," index.html if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then - sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js + sed -i -r "s/\${DELETE_IMAGES}/false/" index.html fi if [ "${SHOW_CONTENT_DIGEST}" = false ] ; then - sed -i -r "s/(showContentDigest[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js + sed -i -r "s/\${SHOW_CONTENT_DIGEST}/false/" index.html fi if [ -n "${CATALOG_ELEMENTS_LIMIT}" ] ; then - sed -i -r "s/(catalogElementsLimit[:=])[^,;]*/\1${CATALOG_ELEMENTS_LIMIT}/" scripts/docker-registry-ui.js + sed -i -r "s/\${CATALOG_ELEMENTS_LIMIT}/${CATALOG_ELEMENTS_LIMIT}/" index.html fi get_nginx_proxy_headers() { @@ -33,8 +34,8 @@ get_nginx_proxy_headers() { done } -if [ -n "${REGISTRY_URL}" ] ; then - sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," /etc/nginx/conf.d/default.conf +if [ -n "${NGINX_PROXY_PASS_URL}" ] ; then + sed -i "s,\${NGINX_PROXY_PASS_URL},${NGINX_PROXY_PASS_URL}," /etc/nginx/conf.d/default.conf sed -i "s^\${NGINX_PROXY_HEADERS}^$(get_nginx_proxy_headers)^" /etc/nginx/conf.d/default.conf sed -i "s,#!,," /etc/nginx/conf.d/default.conf fi diff --git a/debian-static.dockerfile b/debian-static.dockerfile deleted file mode 100644 index 8860fb6..0000000 --- a/debian-static.dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2016-2018 Jones Magloire @Joxit -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -FROM nginx:latest - -LABEL maintainer="Jones MAGLOIRE @Joxit" - -WORKDIR /usr/share/nginx/html/ - -ENV NGINX_PROXY_HEADER_Host '$http_host' - -COPY nginx/default.conf /etc/nginx/conf.d/default.conf -COPY dist/ /usr/share/nginx/html/ -COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js -COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh -COPY favicon.ico /usr/share/nginx/html/ diff --git a/debian.dockerfile b/debian.dockerfile index ee5d31f..2e67843 100644 --- a/debian.dockerfile +++ b/debian.dockerfile @@ -18,5 +18,7 @@ LABEL maintainer="Jones MAGLOIRE @Joxit" WORKDIR /usr/share/nginx/html/ +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh COPY dist/ /usr/share/nginx/html/ COPY favicon.ico /usr/share/nginx/html/ diff --git a/demo/index.html b/demo/index.html index e3275e7..2b94757 100644 --- a/demo/index.html +++ b/demo/index.html @@ -19,12 +19,13 @@ - - + - - + + @@ -35,26 +36,28 @@ - + - - `; + case 'keep': + return opts[output] ? body : ''; + } +}; + +export default function (src, opts) { + let html = fs + .readFileSync(src) + .toString() + .replace(/>\n+\s*/g, '>'); + while (useref.test(html)) { + const [raw, type, output, body] = useref.exec(html); + html = html.replace(raw, generateBalise(type, output, body, opts)); + } + return html; +} diff --git a/rollup/license.js b/rollup/license.js new file mode 100644 index 0000000..f8f9728 --- /dev/null +++ b/rollup/license.js @@ -0,0 +1,18 @@ +export default `/* + * Copyright (C) 2016-2021 Jones Magloire @Joxit + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * @license AGPL + */` \ No newline at end of file diff --git a/src/components/catalog/catalog-element.riot b/src/components/catalog/catalog-element.riot new file mode 100644 index 0000000..15f0caf --- /dev/null +++ b/src/components/catalog/catalog-element.riot @@ -0,0 +1,72 @@ + + + +
+ + + + send + { state.image || state.repo } +
+ { state.images.length } images + expand_more +
+
+
+ +
+ + +
\ No newline at end of file diff --git a/src/components/catalog/catalog.riot b/src/components/catalog/catalog.riot new file mode 100644 index 0000000..55a5fc5 --- /dev/null +++ b/src/components/catalog/catalog.riot @@ -0,0 +1,100 @@ + + + +
+

+ Repositories of { state.registryName } +
{ state.length } images
+

+
+
+
+ +
+ + +
\ No newline at end of file diff --git a/src/components/dialogs/add-registry-url.riot b/src/components/dialogs/add-registry-url.riot new file mode 100644 index 0000000..620da6e --- /dev/null +++ b/src/components/dialogs/add-registry-url.riot @@ -0,0 +1,64 @@ + + + +
Add your Server ?
+
+ + Write your URL without /v2 +
+
+ + Add + + + Cancel + +
+
+ +
\ No newline at end of file diff --git a/src/components/dialogs/change-registry-url.riot b/src/components/dialogs/change-registry-url.riot new file mode 100644 index 0000000..e52ff9c --- /dev/null +++ b/src/components/dialogs/change-registry-url.riot @@ -0,0 +1,78 @@ + + + +
Change your Server ?
+
+ +
+
+ + Change + + + Cancel + +
+
+ + +
\ No newline at end of file diff --git a/src/components/dialogs/dialogs-menu.riot b/src/components/dialogs/dialogs-menu.riot new file mode 100644 index 0000000..cd7ec51 --- /dev/null +++ b/src/components/dialogs/dialogs-menu.riot @@ -0,0 +1,126 @@ + + + + more_vert + + +
+ + + + + +
\ No newline at end of file diff --git a/src/components/dialogs/remove-registry-url.riot b/src/components/dialogs/remove-registry-url.riot new file mode 100644 index 0000000..e694b1e --- /dev/null +++ b/src/components/dialogs/remove-registry-url.riot @@ -0,0 +1,66 @@ + + + +
Remove your Registry Server ?
+
+
    +
  • + + + delete + + { url } + +
  • +
+
+
+ + Close + +
+
+ + +
\ No newline at end of file diff --git a/src/components/docker-registry-ui.riot b/src/components/docker-registry-ui.riot new file mode 100644 index 0000000..e6692a7 --- /dev/null +++ b/src/components/docker-registry-ui.riot @@ -0,0 +1,129 @@ + + +
+ + + + +
+
+ + + + + + + + + + + + +
+ + +
\ No newline at end of file diff --git a/src/components/tag-history/tag-history-element.riot b/src/components/tag-history/tag-history-element.riot new file mode 100644 index 0000000..59cd844 --- /dev/null +++ b/src/components/tag-history/tag-history-element.riot @@ -0,0 +1,46 @@ + + +
{ state.icon } +

{ state.name }

+
+
{ state.value }
+
{ value }
+ +
\ No newline at end of file diff --git a/src/components/tag-history/tag-history.riot b/src/components/tag-history/tag-history.riot new file mode 100644 index 0000000..fc2a850 --- /dev/null +++ b/src/components/tag-history/tag-history.riot @@ -0,0 +1,185 @@ + + + +
+ + arrow_back + +

+ History of { props.image }:{ props.tag } history +

+
+
+
+ +
+ + + + + + + +
\ No newline at end of file diff --git a/src/components/tag-list/copy-to-clipboard.riot b/src/components/tag-list/copy-to-clipboard.riot new file mode 100644 index 0000000..13e0350 --- /dev/null +++ b/src/components/tag-list/copy-to-clipboard.riot @@ -0,0 +1,58 @@ + + +
+ + + content_copy + +
+ +
\ No newline at end of file diff --git a/src/components/tag-list/image-content-digest.riot b/src/components/tag-list/image-content-digest.riot new file mode 100644 index 0000000..3cd1d2b --- /dev/null +++ b/src/components/tag-list/image-content-digest.riot @@ -0,0 +1,50 @@ + + +
{ state.displayId }
+ +
\ No newline at end of file diff --git a/src/tags/image-date.riot b/src/components/tag-list/image-date.riot similarity index 55% rename from src/tags/image-date.riot rename to src/components/tag-list/image-date.riot index 03b8bbd..9827ecd 100644 --- a/src/tags/image-date.riot +++ b/src/components/tag-list/image-date.riot @@ -1,5 +1,5 @@ -
{ registryUI.dateFormat(this.date) } ago
-
\ No newline at end of file diff --git a/src/tags/image-size.riot b/src/components/tag-list/image-size.riot similarity index 61% rename from src/tags/image-size.riot rename to src/components/tag-list/image-size.riot index c262fb5..619af6e 100644 --- a/src/tags/image-size.riot +++ b/src/components/tag-list/image-size.riot @@ -1,5 +1,5 @@ -
{ registryUI.bytesToSize(this.size) }
-
\ No newline at end of file diff --git a/src/tags/image-tag.riot b/src/components/tag-list/image-tag.riot similarity index 65% rename from src/tags/image-tag.riot rename to src/components/tag-list/image-tag.riot index 484a569..ad5dc62 100644 --- a/src/tags/image-tag.riot +++ b/src/components/tag-list/image-tag.riot @@ -1,5 +1,5 @@ -
{ opts.image.tag }
-
\ No newline at end of file diff --git a/src/tags/tag-history-element.riot b/src/components/tag-list/pagination.riot similarity index 53% rename from src/tags/tag-history-element.riot rename to src/components/tag-list/pagination.riot index db92292..99ce37a 100644 --- a/src/tags/tag-history-element.riot +++ b/src/components/tag-list/pagination.riot @@ -1,5 +1,5 @@ - -
{ registryUI.getHistoryIcon(entry.key) } -

{ entry.key.replace('_', ' ') }

+ +
+
+ + { p.icon } +
{ p.page }
+
+
-
{ entry.value }
-
{ e }
- \ No newline at end of file + +
\ No newline at end of file diff --git a/src/components/tag-list/remove-image.riot b/src/components/tag-list/remove-image.riot new file mode 100644 index 0000000..2f78da5 --- /dev/null +++ b/src/components/tag-list/remove-image.riot @@ -0,0 +1,84 @@ + + + + delete + + + + + \ No newline at end of file diff --git a/src/tags/tag-history-button.riot b/src/components/tag-list/tag-history-button.riot similarity index 64% rename from src/tags/tag-history-button.riot rename to src/components/tag-list/tag-history-button.riot index fdb1c73..77ca9a6 100644 --- a/src/tags/tag-history-button.riot +++ b/src/components/tag-list/tag-history-button.riot @@ -1,5 +1,5 @@ - + history - \ No newline at end of file diff --git a/src/components/tag-list/tag-list.riot b/src/components/tag-list/tag-list.riot new file mode 100644 index 0000000..a73a2be --- /dev/null +++ b/src/components/tag-list/tag-list.riot @@ -0,0 +1,161 @@ + + + +
+ + arrow_back + +

+ Tags of { props.image } +
+ Sourced from { state.registryName + '/' + props.image } +
+
{ state.tags.length } tags
+

+
+
+ +
+ +
+ + + + + + + + +
\ No newline at end of file diff --git a/src/components/tag-list/tag-table.riot b/src/components/tag-list/tag-table.riot new file mode 100644 index 0000000..37ebf0c --- /dev/null +++ b/src/components/tag-list/tag-table.riot @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
Creation dateSizeContent DigestTag + History + + + + delete + +
+ + + + + + + + + + + + + +
+
+ +
\ No newline at end of file diff --git a/src/index.html b/src/index.html index 91d7af5..7087846 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ + - - @@ -36,13 +34,21 @@ - - + + + + + + + + + - - @@ -67,4 +73,4 @@ - + \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..97b5af5 --- /dev/null +++ b/src/index.js @@ -0,0 +1,39 @@ +import { component, register } from 'riot'; + +import { + MaterialCard, + MaterialSpinner, + MaterialNavbar, + MaterialFooter, + MaterialButton, + MaterialWaves, + MaterialCheckbox, + MaterialTabs, + MaterialSnackbar, + MaterialDropdownList, + MaterialPopup, + MaterialInput, +} from 'riot-mui'; + +import DockerRegistryUI from './components/docker-registry-ui.riot'; + +import './style.scss'; + +register('material-card', MaterialCard); +register('material-footer', MaterialFooter); +register('material-navbar', MaterialNavbar); +register('material-spinner', MaterialSpinner); +register('material-button', MaterialButton); +register('material-waves', MaterialWaves); +register('material-checkbox', MaterialCheckbox); +register('material-snackbar', MaterialSnackbar); +register('material-tabs', MaterialTabs); +register('material-dropdown-list', MaterialDropdownList); +register('material-popup', MaterialPopup); +register('material-input', MaterialInput); + +const createApp = component(DockerRegistryUI); +const tags = document.getElementsByTagName('docker-registry-ui'); +for (let i = 0; i < tags.length; i++) { + createApp(tags.item(i)); +} diff --git a/src/material-icons.css b/src/material-icons.scss similarity index 100% rename from src/material-icons.css rename to src/material-icons.scss diff --git a/src/roboto.css b/src/roboto.scss similarity index 100% rename from src/roboto.css rename to src/roboto.scss diff --git a/src/scripts/docker-image.js b/src/scripts/docker-image.js new file mode 100644 index 0000000..3a86d56 --- /dev/null +++ b/src/scripts/docker-image.js @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2016-2021 Jones Magloire @Joxit + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Http } from './http'; +import { isDigit, eventTransfer, ERROR_CAN_NOT_READ_CONTENT_DIGEST } from './utils'; +import observable from '@riotjs/observable'; + +const tagReduce = (acc, e) => { + if (acc.length > 0 && isDigit(acc[acc.length - 1].charAt(0)) == isDigit(e)) { + acc[acc.length - 1] += e; + } else { + acc.push(e); + } + return acc; +}; + +export function compare(e1, e2) { + const tag1 = e1.tag.match(/./g).reduce(tagReduce, []); + const tag2 = e2.tag.match(/./g).reduce(tagReduce, []); + + for (var i = 0; i < tag1.length && i < tag2.length; i++) { + const compare = tag1[i].localeCompare(tag2[i]); + if (isDigit(tag1[i].charAt(0)) && isDigit(tag2[i].charAt(0))) { + const diff = tag1[i] - tag2[i]; + if (diff != 0) { + return diff; + } + } else if (compare != 0) { + return compare; + } + } + return e1.tag.length - e2.tag.length; +} + +export class DockerImage { + constructor(name, tag, list, registryUrl, onNotify) { + this.name = name; + this.tag = tag; + this.list = list; + this.registryUrl = registryUrl; + this.chars = 0; + this.onNotify = onNotify; + observable(this); + this.on('get-size', function () { + if (this.size !== undefined) { + return this.trigger('size', this.size); + } + return this.fillInfo(); + }); + this.on('get-sha256', function () { + if (this.size !== undefined) { + return this.trigger('sha256', this.sha256); + } + return this.fillInfo(); + }); + this.on('get-date', function () { + if (this.creationDate !== undefined) { + return this.trigger('creation-date', this.creationDate); + } + return this.fillInfo(); + }); + this.on('content-digest-chars', function (chars) { + this.chars = chars; + }); + this.on('get-content-digest-chars', function () { + return this.trigger('content-digest-chars', this.chars); + }); + this.on('get-content-digest', function () { + if (this.digest !== undefined) { + return this.trigger('content-digest', this.digest); + } + return this.fillInfo(); + }); + } + fillInfo() { + if (this._fillInfoWaiting) { + return; + } + this._fillInfoWaiting = true; + const oReq = new Http(); + const self = this; + oReq.addEventListener('loadend', function () { + if (this.status == 200 || this.status == 202) { + const response = JSON.parse(this.responseText); + if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') { + self.trigger('list', response); + const manifest = response.manifests[0]; + const image = new DockerImage(self.name, manifest.digest, false, self.registryUrl, self.onNotify); + eventTransfer(image, self); + image.fillInfo(); + self.variants = [image]; + return; + } + self.size = response.layers.reduce(function (acc, e) { + return acc + e.size; + }, 0); + self.sha256 = response.config.digest; + self.layers = response.layers; + self.trigger('size', self.size); + self.trigger('sha256', self.sha256); + oReq.getContentDigest(function (digest) { + self.digest = digest; + self.trigger('content-digest', digest); + if (!digest) { + self.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST); + } + }); + self.getBlobs(response.config.digest); + } else if (this.status == 404) { + self.onNotify(`Manifest for ${self.name}:${self.tag} not found`, true); + } else { + self.onNotify(this.responseText); + } + }); + oReq.open('GET', this.registryUrl + '/v2/' + self.name + '/manifests/' + self.tag); + oReq.setRequestHeader( + 'Accept', + 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' + + (self.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : '') + ); + oReq.send(); + } + getBlobs(blob) { + const oReq = new Http(); + const self = this; + oReq.addEventListener('loadend', function () { + if (this.status == 200 || this.status == 202) { + const response = JSON.parse(this.responseText); + self.creationDate = new Date(response.created); + self.blobs = response; + self.blobs.history + .filter(function (e) { + return !e.empty_layer; + }) + .forEach(function (e, i) { + e.size = self.layers[i].size; + e.id = self.layers[i].digest.replace('sha256:', ''); + }); + self.blobs.id = blob.replace('sha256:', ''); + self.trigger('creation-date', self.creationDate); + self.trigger('blobs', self.blobs); + } else if (this.status == 404) { + self.onNotify(`Blobs for ${self.name}:${self.tag} not found`, true); + } else { + self.onNotify(this.responseText); + } + }); + oReq.open('GET', this.registryUrl + '/v2/' + self.name + '/blobs/' + blob); + oReq.setRequestHeader( + 'Accept', + 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' + ); + oReq.send(); + } +} diff --git a/src/scripts/http.js b/src/scripts/http.js index 07f5aea..cda58b5 100644 --- a/src/scripts/http.js +++ b/src/scripts/http.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2019 Jones Magloire @Joxit + * Copyright (C) 2016-2021 Jones Magloire @Joxit * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -14,47 +14,47 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -function Http() { - this.oReq = new XMLHttpRequest(); - this.oReq.hasHeader = Http.hasHeader; - this.oReq.getErrorMessage = Http.getErrorMessage; - this._events = {}; - this._headers = {}; -} -Http.prototype.getContentDigest = function(cb) { - if (this.oReq.hasHeader('Docker-Content-Digest')) { - // Same origin or advanced CORS headers set: - // 'Access-Control-Expose-Headers: Docker-Content-Digest' - cb(this.oReq.getResponseHeader('Docker-Content-Digest')) - } else if (window.crypto && window.TextEncoder) { - crypto.subtle.digest( - 'SHA-256', - new TextEncoder().encode(this.oReq.responseText) - ).then(function (buffer) { - cb( - 'sha256:' + Array.from( - new Uint8Array(buffer) - ).map(function(byte) { - return byte.toString(16).padStart(2, '0'); - }).join('') - ); - }) - } else { - // IE and old Edge - // simply do not call the callback and skip the setup downstream +export class Http { + constructor() { + this.oReq = new XMLHttpRequest(); + this.oReq.hasHeader = hasHeader; + this.oReq.getErrorMessage = getErrorMessage; + this._events = {}; + this._headers = {}; } -}; -Http.prototype.addEventListener = function(e, f) { - this._events[e] = f; - const self = this; - switch (e) { - case 'loadend': - { - self.oReq.addEventListener('loadend', function() { + getContentDigest(cb) { + if (this.oReq.hasHeader('Docker-Content-Digest')) { + // Same origin or advanced CORS headers set: + // 'Access-Control-Expose-Headers: Docker-Content-Digest' + cb(this.oReq.getResponseHeader('Docker-Content-Digest')); + } else if (window.crypto && window.TextEncoder) { + crypto.subtle.digest('SHA-256', new TextEncoder().encode(this.oReq.responseText)).then(function (buffer) { + cb( + 'sha256:' + + Array.from(new Uint8Array(buffer)) + .map(function (byte) { + return byte.toString(16).padStart(2, '0'); + }) + .join('') + ); + }); + } else { + // IE and old Edge + // simply do not call the callback and skip the setup downstream + } + } + + addEventListener(e, f) { + this._events[e] = f; + const self = this; + switch (e) { + case 'loadend': { + self.oReq.addEventListener('loadend', function () { if (this.status == 401) { const req = new XMLHttpRequest(); + req._url = self._url; req.open(self._method, self._url); for (key in self._events) { req.addEventListener(key, self._events[key]); @@ -73,53 +73,69 @@ Http.prototype.addEventListener = function(e, f) { }); break; } - case 'load': - { - self.oReq.addEventListener('load', function() { + case 'load': { + self.oReq.addEventListener('load', function () { if (this.status !== 401) { f.bind(this)(); } }); break; } - default: - { - self.oReq.addEventListener(e, function() { + default: { + self.oReq.addEventListener(e, function () { f.bind(this)(); }); break; } + } } + + setRequestHeader(header, value) { + this.oReq.setRequestHeader(header, value); + this._headers[header] = value; + } + + open(m, u) { + this._method = m; + this._url = u; + this.oReq._url = u; + this.oReq.open(m, u); + } + + send() { + this.oReq.send(); + } +} + +const hasHeader = function (header) { + return this.getAllResponseHeaders() + .split('\n') + .some(function (h) { + return new RegExp('^' + header + ':', 'i').test(h); + }); }; -Http.prototype.setRequestHeader = function(header, value) { - this.oReq.setRequestHeader(header, value); - this._headers[header] = value; -}; - -Http.prototype.open = function(m, u) { - this._method = m; - this._url = u; - this.oReq.open(m, u); -}; - -Http.prototype.send = function() { - this.oReq.send(); -}; - -Http.hasHeader = function(header) { - return this.getAllResponseHeaders().split('\n').some(function(h) { - return new RegExp('^' + header + ':', 'i').test(h); - }); -}; - -Http.getErrorMessage = function() { - if (registryUI.url() && registryUI.url().match('^http://') && window.location.protocol === 'https:') { - return 'Mixed Content: The page at `' + window.location.origin + '` was loaded over HTTPS, but requested an insecure server endpoint `' + registryUI.url() + '`. This request has been blocked; the content must be served over HTTPS.'; - } else if (!registryUI.url()) { +const getErrorMessage = function () { + if (this._url.match('^http://') && window.location.protocol === 'https:') { + return ( + 'Mixed Content: The page at `' + + window.location.origin + + '` was loaded over HTTPS, but requested an insecure server endpoint `' + + new URL(this._url).origin + + '`. This request has been blocked; the content must be served over HTTPS.' + ); + } else if (!this._url || !this._url.match('^http')) { return 'Incorrect server endpoint.'; } else if (this.withCredentials && !this.hasHeader('Access-Control-Allow-Credentials')) { - return 'The `Access-Control-Allow-Credentials` header in the response is missing and must be set to `true` when the request\'s credentials mode is on. Origin `'+ registryUI.url() +'` is therefore not allowed access.'; + return ( + "The `Access-Control-Allow-Credentials` header in the response is missing and must be set to `true` when the request's credentials mode is on. Origin `" + + new URL(this._url).origin + + '` is therefore not allowed access.' + ); } - return 'An error occured: Check your connection and your registry must have `Access-Control-Allow-Origin` header set to `' + window.location.origin + '`'; -}; \ No newline at end of file + return ( + 'An error occured: Check your connection and your registry must have `Access-Control-Allow-Origin` header set to `' + + window.location.origin + + '`' + ); +}; diff --git a/src/scripts/router.js b/src/scripts/router.js new file mode 100644 index 0000000..e8fc42e --- /dev/null +++ b/src/scripts/router.js @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016-2021 Jones Magloire @Joxit + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { router, getCurrentRoute } from '@riotjs/route'; +import { encodeURI, decodeURI } from './utils'; + +function getQueryParams() { + const queries = {}; + window.location.search + .slice(1) + .split('&') + .forEach((qs) => { + const splitIndex = qs.indexOf('='); + queries[qs.slice(0, splitIndex)] = splitIndex < 0 ? '' : qs.slice(splitIndex + 1); + }); + return queries; +} + +function updateQueryParams(qs) { + const queryParams = getQueryParams(); + for (let key in qs) { + if (qs[key] === null) { + delete queryParams[key]; + } else { + queryParams[key] = qs[key]; + } + } + return queryParams; +} + +function toSearchString(queries) { + let search = []; + for (let key in queries) { + if (key && queries[key] !== undefined) { + search.push(`${key}=${queries[key]}`); + } + } + return search.length === 0 ? '' : `?${search.join('&')}`; +} + +function baseUrl(qs) { + const location = window.location; + const queryParams = updateQueryParams(qs); + return location.origin + location.pathname + toSearchString(queryParams); +} + +export default { + home() { + router.push(baseUrl({ page: null })); + }, + taglist(image) { + router.push(`${baseUrl({ page: null })}#!/taglist/${image}`); + }, + getTagListImage() { + return getCurrentRoute().replace(/^.*(#!)?\/?taglist\//, ''); + }, + history(image, tag) { + router.push(`${baseUrl({ page: null })}#!/taghistory/image/${image}/tag/${tag}`); + }, + getTagHistoryImage() { + return getCurrentRoute().replace(/^.*(#!)?\/?taghistory\/image\/(.*)\/tag\/(.*)\/?$/, '$2'); + }, + getTagHistoryTag() { + return getCurrentRoute().replace(/^.*(#!)?\/?taghistory\/image\/(.*)\/tag\/(.*)\/?$/, '$3'); + }, + updateQueryString(qs) { + const search = toSearchString(updateQueryParams(qs)); + history.pushState(null, '', search + window.location.hash); + }, + updateUrlQueryParam(url) { + this.updateQueryString({ url: encodeURI(url) }); + }, + getUrlQueryParam() { + const queries = getQueryParams(); + const url = queries['url']; + if (url) { + try { + return decodeURI(url); + } catch (e) { + console.error(`Can't decode query parameter URL: ${url}`, e); + } + } + }, + updatePageQueryParam(page) { + this.updateQueryString({ page }); + }, + getPageQueryParam() { + const queries = getQueryParams(); + return queries['page']; + }, +}; diff --git a/src/scripts/script.js b/src/scripts/script.js deleted file mode 100644 index 7553643..0000000 --- a/src/scripts/script.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2016-2019 Jones Magloire @Joxit - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var registryUI = {} -registryUI.URL_QUERY_PARAM_REGEX = /[&?]url=/; -registryUI.URL_PARAM_REGEX = /^url=/; -registryUI.showContentDigest = true; -registryUI.catalogElementsLimit = 100000; - -registryUI.url = function(byPassQueryParam) { - if (!registryUI._url) { - const url = registryUI.getUrlQueryParam(); - if (url) { - try { - registryUI._url = registryUI.decodeURI(url); - return registryUI._url; - } catch (e) { - console.log(e); - } - } - registryUI._url = registryUI.getRegistryServer(0); - } - return registryUI._url; -} -registryUI.name = function() { - return registryUI.stripHttps(registryUI.url()); -} -registryUI.getRegistryServer = function(i) { - try { - const res = JSON.parse(localStorage.getItem('registryServer')); - if (res instanceof Array) { - return (!isNaN(i)) ? res[i] : res.map(function(url) { - return url.trim().replace(/\/*$/, ''); - }); - } - } catch (e) {} - return (!isNaN(i)) ? '' : []; -} -registryUI.addServer = function(url) { - const registryServer = registryUI.getRegistryServer(); - url = url.trim().replace(/\/*$/, ''); - const index = registryServer.indexOf(url); - if (index != -1) { - return; - } - registryServer.push(url); - if (!registryUI._url) { - registryUI.updateHistory(url); - } - localStorage.setItem('registryServer', JSON.stringify(registryServer)); -}; -registryUI.changeServer = function(url) { - var registryServer = registryUI.getRegistryServer(); - url = url.trim().replace(/\/*$/, ''); - const index = registryServer.indexOf(url); - if (index == -1) { - return; - } - registryServer.splice(index, 1); - registryServer = [url].concat(registryServer); - registryUI.updateHistory(url); - localStorage.setItem('registryServer', JSON.stringify(registryServer)); -}; -registryUI.removeServer = function(url) { - const registryServer = registryUI.getRegistryServer(); - url = url.trim().replace(/\/*$/, ''); - const index = registryServer.indexOf(url); - if (index == -1) { - return; - } - registryServer.splice(index, 1); - localStorage.setItem('registryServer', JSON.stringify(registryServer)); - if (url == registryUI.url()) { - registryUI.updateHistory(registryUI.getRegistryServer(0)); - route(''); - } -} - -registryUI.updateHistory = function(url) { - registryUI.updateQueryString({ url: registryUI.encodeURI(url) }) - registryUI._url = url; -} - -registryUI.getUrlQueryParam = function () { - const search = window.location.search; - if (registryUI.URL_QUERY_PARAM_REGEX.test(search)) { - const param = search.split(/^\?|&/).find(function(param) { - return param && registryUI.URL_PARAM_REGEX.test(param); - }); - return param ? param.replace(registryUI.URL_PARAM_REGEX, '') : param; - } -}; - -registryUI.encodeURI = function(url) { - if (!url) { return; } - return url.indexOf('&') < 0 ? window.encodeURIComponent(url) : btoa(url); -}; - -registryUI.decodeURI = function(url) { - if (!url) { return; } - return url.startsWith('http') ? window.decodeURIComponent(url) : atob(url); -}; - -registryUI.isImageRemoveActivated = true; -registryUI.catalog = {}; -registryUI.taglist = {}; -registryUI.taghistory = {}; - -window.addEventListener('DOMContentLoaded', function() { - riot.mount('*'); -}); diff --git a/src/scripts/static.js b/src/scripts/static.js deleted file mode 100644 index eb5cace..0000000 --- a/src/scripts/static.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2016-2019 Jones Magloire @Joxit - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var registryUI = {} -registryUI.url = function() { - var url = '${URL}'; - if (!url) { - url = window.location.origin + window.location.pathname; - return url.endsWith('/') ? url.substr(0, url.length - 1) : url; - } - return url; -}; -registryUI.name = function() { - const name = '${REGISTRY_TITLE}'; - if (name) { - // the user can strip the http prefix if they wish - return name; - } - return registryUI.stripHttps(registryUI.url()); -}; -registryUI.pullUrl = '${PULL_URL}'; -registryUI.isImageRemoveActivated = true; -registryUI.showContentDigest = true; -registryUI.catalogElementsLimit = 100000; -registryUI.catalog = {}; -registryUI.taglist = {}; -registryUI.taghistory = {}; - -window.addEventListener('DOMContentLoaded', function() { - riot.mount('*'); -}); diff --git a/src/scripts/utils.js b/src/scripts/utils.js index 8623dfa..a02dc55 100644 --- a/src/scripts/utils.js +++ b/src/scripts/utils.js @@ -1,5 +1,4 @@ - -registryUI.bytesToSize = function (bytes) { +export function bytesToSize(bytes) { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes == undefined || isNaN(bytes)) { return '?'; @@ -8,13 +7,26 @@ registryUI.bytesToSize = function (bytes) { } const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return Math.ceil(bytes / Math.pow(1024, i)) + ' ' + sizes[i]; -}; +} -registryUI.dateFormat = function(date) { +export function dateFormat(date) { if (date === undefined) { return ''; } - const labels = ['a second', 'seconds', 'a minute', 'minutes', 'an hour', 'hours', 'a day', 'days', 'a month', 'months', 'a year', 'years']; + const labels = [ + 'a second', + 'seconds', + 'a minute', + 'minutes', + 'an hour', + 'hours', + 'a day', + 'days', + 'a month', + 'months', + 'a year', + 'years', + ]; const maxSeconds = [1, 60, 3600, 86400, 2592000, 31104000, Infinity]; const diff = (new Date() - date) / 1000; for (var i = 0; i < maxSeconds.length - 1; i++) { @@ -24,10 +36,9 @@ registryUI.dateFormat = function(date) { return Math.floor(diff / maxSeconds[i]) + ' ' + labels[i * 2 + 1]; } } -}; +} - -registryUI.getHistoryIcon = function(attribute) { +export function getHistoryIcon(attribute) { switch (attribute) { case 'architecture': return 'memory'; @@ -63,29 +74,39 @@ registryUI.getHistoryIcon = function(attribute) { case 'ExposedPorts': return 'router'; default: - '' + ''; } } -registryUI.getPage = function(elts, page, limit) { - if (!limit) { limit = 100; } - if (!elts) { return []; } +export function getPage(elts, page, limit) { + if (!limit) { + limit = 100; + } + if (!elts) { + return []; + } return elts.slice((page - 1) * limit, limit * page); } -registryUI.getNumPages = function(elts, limit) { - if (!limit) { limit = 100; } - if (!elts) { return 0; } +export function getNumPages(elts, limit) { + if (!limit) { + limit = 100; + } + if (!elts) { + return 0; + } return Math.trunc(elts.length / limit) + 1; } -registryUI.getPageLabels = function(page, nPages) { +export function getPageLabels(page, nPages) { var pageLabels = []; var maxItems = 10; - if (nPages === 1) { return pageLabels; } + if (nPages === 1) { + return pageLabels; + } if (page !== 1 && nPages >= maxItems) { - pageLabels.push({'icon': 'first_page', page: 1}); - pageLabels.push({'icon': 'chevron_left', page: page - 1}); + pageLabels.push({ 'icon': 'first_page', page: 1 }); + pageLabels.push({ 'icon': 'chevron_left', page: page - 1 }); } var start = Math.round(Math.max(1, Math.min(page - maxItems / 2, nPages - maxItems + 1))); for (var i = start; i < Math.min(nPages + 1, start + maxItems); i++) { @@ -93,35 +114,62 @@ registryUI.getPageLabels = function(page, nPages) { page: i, current: i === page, 'space-left': page === 1 && nPages > maxItems, - 'space-right': page === nPages && nPages > maxItems + 'space-right': page === nPages && nPages > maxItems, }); } if (page !== nPages && nPages >= maxItems) { - pageLabels.push({'icon': 'chevron_right', page: page + 1}); - pageLabels.push({'icon': 'last_page', page: nPages}); + pageLabels.push({ 'icon': 'chevron_right', page: page + 1 }); + pageLabels.push({ 'icon': 'last_page', page: nPages }); } return pageLabels; } -registryUI.updateQueryString = function(qs) { - var search = ''; - for (var key in qs) { - if (qs[key] !== undefined) { - search += (search.length > 0 ? '&' : '?') +key + '=' + qs[key]; - } - } - history.pushState(null, '', search + window.location.hash); -} - -registryUI.stripHttps = function (url) { +export function stripHttps(url) { if (!url) { return ''; } return url.replace(/^https?:\/\//, ''); +} + +export function eventTransfer(from, to) { + from.on('*', function (event, param) { + to.trigger(event, param); + }); +} + +export function isDigit(char) { + return char >= '0' && char <= '9'; +} + +export const ERROR_CAN_NOT_READ_CONTENT_DIGEST = { + message: + 'Access on registry response was blocked. Try adding the header ' + + '`Access-Control-Expose-Headers: Docker-Content-Digest`' + + ' to your proxy or registry: ' + + 'https://docs.docker.com/registry/configuration/#http', + isError: true, }; -registryUI.eventTransfer = function(from, to) { - from.on('*', function(event, param) { - to.trigger(event, param); - }) +export function getRegistryServers(i) { + try { + const res = JSON.parse(localStorage.getItem('registryServer')); + if (res instanceof Array) { + return !isNaN(i) ? res[i] : res.map((url) => url.trim().replace(/\/*$/, '')); + } + } catch (e) {} + return !isNaN(i) ? '' : []; +} + +export function encodeURI(url) { + if (!url) { + return; + } + return url.indexOf('&') < 0 ? window.encodeURIComponent(url) : btoa(url); +} + +export function decodeURI(url) { + if (!url) { + return; + } + return url.startsWith('http') ? window.decodeURIComponent(url) : atob(url); } \ No newline at end of file diff --git a/src/style.css b/src/style.scss similarity index 90% rename from src/style.css rename to src/style.scss index 9cc7c3a..64bd40c 100644 --- a/src/style.css +++ b/src/style.scss @@ -14,7 +14,22 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +@import 'riot-mui/src/material-elements/material-navbar/material-navbar.scss'; +@import 'riot-mui/src/material-elements/material-footer/material-footer.scss'; +@import 'riot-mui/src/material-elements/material-card/material-card.scss'; +@import 'riot-mui/src/material-elements/material-spinner/material-spinner.scss'; +@import 'riot-mui/src/material-elements/material-button/material-button.scss'; +@import 'riot-mui/src/material-elements/material-waves/material-waves.scss'; +@import 'riot-mui/src/material-elements/material-checkbox/material-checkbox.scss'; +@import 'riot-mui/src/material-elements/material-tabs/material-tabs.scss'; +@import 'riot-mui/src/material-elements/material-snackbar/material-snackbar.scss'; +@import 'riot-mui/src/material-elements/material-dropdown-list/material-dropdown-list.scss'; +@import 'riot-mui/src/material-elements/material-popup/material-popup.scss'; +@import 'riot-mui/src/material-elements/material-input/material-input.scss'; +@import './roboto.scss'; +@import './material-icons.scss'; + html > body { font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif !important; } @@ -106,6 +121,10 @@ material-navbar { height: 64px; } +material-navbar nav-wrapper { + display: flex; +} + .logo { padding: 0 16px 0 72px; text-decoration: none; @@ -310,59 +329,6 @@ material-snackbar .toast { height: auto; } -menu { - position: absolute; - top: 0px; - right: 16px; - color: #000; -} - -menu .overlay { - position: fixed; - height: 100%; - width: 100%; - top: 0; - right: 0; - z-index: 1; -} - -#menu-control-button { - background: rgba(255, 255, 255, 0); - float: right; -} - -#menu-control-button i { - color: #fff; - font-size: 24px; -} - -#menu-control-dropdown { - display: inline-block; - position: relative; -} - -.dropdown { - min-width: 124px; - padding: 8px 0; - margin: 0; -} - -dropdown-item, #menu-control-dropdown p { - padding: 0 16px; - margin: auto; - line-height: 48px; - height: 48px; - cursor: pointer; -} - -#menu-control-dropdown p:hover { - background-color: #eee; -} - -#menu-control-dropdown p:active, .material-button-active:active { - background-color: #e0e0e0; -} - material-popup material-button, pagination material-button { background-color: #fff; @@ -392,27 +358,6 @@ material-footer { padding: 0.5em 1em; } -.select-padding { - padding: 20px 0; -} - -select { - position: relative; - outline: 0; - box-shadow: none; - padding: 0; - width: 100%; - background: 0 0; - border: none; - font-weight: 400; - line-height: 24px; - height: 24px; - border-bottom: 1px solid #2f6975; - appearance: none; - -moz-appearance: none; - -webkit-appearance: none; -} - .copy-to-clipboard { padding-left: 5px; } diff --git a/src/tags/app.riot b/src/tags/app.riot deleted file mode 100644 index f23f47b..0000000 --- a/src/tags/app.riot +++ /dev/null @@ -1,290 +0,0 @@ - - -
- - - - -
-
- - - - - - - -
- - -
\ No newline at end of file diff --git a/src/tags/catalog-element.riot b/src/tags/catalog-element.riot deleted file mode 100644 index fdc0da8..0000000 --- a/src/tags/catalog-element.riot +++ /dev/null @@ -1,61 +0,0 @@ - - - -
- - - - send - { typeof opts.item === "string" ? opts.item : opts.item.repo } -
- { opts.item.images && opts.item.images.length } images - expand_more -
-
-
- -
- - -
diff --git a/src/tags/catalog.riot b/src/tags/catalog.riot deleted file mode 100644 index 70864f8..0000000 --- a/src/tags/catalog.riot +++ /dev/null @@ -1,78 +0,0 @@ - - - - -
-

- Repositories of { registryUI.name() } -
{ registryUI.catalog.length } images
-

-
-
-
- -
- - - -
diff --git a/src/tags/copy-to-clipboard.riot b/src/tags/copy-to-clipboard.riot deleted file mode 100644 index f8478c7..0000000 --- a/src/tags/copy-to-clipboard.riot +++ /dev/null @@ -1,50 +0,0 @@ - - -
- - - content_copy - -
- -
\ No newline at end of file diff --git a/src/tags/dialogs/add.riot b/src/tags/dialogs/add.riot deleted file mode 100644 index c404c8b..0000000 --- a/src/tags/dialogs/add.riot +++ /dev/null @@ -1,59 +0,0 @@ - - - -
Add your Server ?
-
- - Write your URL without /v2 -
-
- Add - Cancel -
-
- - -
diff --git a/src/tags/dialogs/change.riot b/src/tags/dialogs/change.riot deleted file mode 100644 index 1801703..0000000 --- a/src/tags/dialogs/change.riot +++ /dev/null @@ -1,61 +0,0 @@ - - - -
Change your Server ?
-
-
- -
-
-
- Change - Cancel -
-
- -
\ No newline at end of file diff --git a/src/tags/dialogs/menu.riot b/src/tags/dialogs/menu.riot deleted file mode 100644 index 64c3aa9..0000000 --- a/src/tags/dialogs/menu.riot +++ /dev/null @@ -1,45 +0,0 @@ - - - - more_vert - - -

Add URL

-

Change URL

-

Remove URL

-
-
- -
diff --git a/src/tags/dialogs/remove.riot b/src/tags/dialogs/remove.riot deleted file mode 100644 index 827e272..0000000 --- a/src/tags/dialogs/remove.riot +++ /dev/null @@ -1,61 +0,0 @@ - - - - -
Remove your Registry Server ?
-
- -
-
- - Close - -
-
- -
diff --git a/src/tags/image-content-digest.riot b/src/tags/image-content-digest.riot deleted file mode 100644 index 5940aad..0000000 --- a/src/tags/image-content-digest.riot +++ /dev/null @@ -1,48 +0,0 @@ - - -
{ this.display_id }
- -
\ No newline at end of file diff --git a/src/tags/pagination.riot b/src/tags/pagination.riot deleted file mode 100644 index 046d761..0000000 --- a/src/tags/pagination.riot +++ /dev/null @@ -1,40 +0,0 @@ - - - -
-
- - { p.icon } -
{ p.page }
-
-
-
- - -
\ No newline at end of file diff --git a/src/tags/remove-image.riot b/src/tags/remove-image.riot deleted file mode 100644 index 25fbb20..0000000 --- a/src/tags/remove-image.riot +++ /dev/null @@ -1,78 +0,0 @@ - - - - delete - - - - diff --git a/src/tags/tag-history.riot b/src/tags/tag-history.riot deleted file mode 100644 index 27fd9b4..0000000 --- a/src/tags/tag-history.riot +++ /dev/null @@ -1,160 +0,0 @@ - - - -
- - arrow_back - -

- History of { registryUI.taghistory.image }:{ registryUI.taghistory.tag } history -

-
-
-
- -
- - - - - - - -
\ No newline at end of file diff --git a/src/tags/taglist.riot b/src/tags/taglist.riot deleted file mode 100644 index aa5f633..0000000 --- a/src/tags/taglist.riot +++ /dev/null @@ -1,245 +0,0 @@ - - - - -
- - arrow_back - -

- Tags of { registryUI.taglist.name } -
- Sourced from { registryUI.name() + '/' + registryUI.taglist.name } -
-
{ registryUI.taglist.tags.length } tags
-

-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
Creation dateSizeContent DigestTag - History 0 } if="{ registryUI.isImageRemoveActivated }"> - - 0 }> - delete -
- - - - - - - - - - - - - -
-
- - - -
\ No newline at end of file diff --git a/static.dockerfile b/static.dockerfile deleted file mode 100644 index 20bf1a6..0000000 --- a/static.dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2016-2018 Jones Magloire @Joxit -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -FROM nginx:alpine - -LABEL maintainer="Jones MAGLOIRE @Joxit" - -WORKDIR /usr/share/nginx/html/ - -ENV NGINX_PROXY_HEADER_Host '$http_host' - -COPY nginx/default.conf /etc/nginx/conf.d/default.conf -COPY dist/ /usr/share/nginx/html/ -COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js -COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh -COPY favicon.ico /usr/share/nginx/html/