mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2026-02-19 21:29:51 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c84c9f36e6 | ||
|
|
1939b47677 | ||
|
|
736d527cc8 | ||
|
|
c310845c18 | ||
|
|
f0c7232843 | ||
|
|
49fcba3f6c | ||
|
|
ab12cceefc | ||
|
|
3af4438815 | ||
|
|
f826381681 | ||
|
|
ba2e0b119e | ||
|
|
772d19c18f | ||
|
|
ba6d817b41 | ||
|
|
19e72e4a5f | ||
|
|
05cbb51125 | ||
|
|
7c0874694a | ||
|
|
126509d7fa | ||
|
|
e1fd515279 | ||
|
|
befbd0bcfb |
2
.github/workflows/main.yml
vendored
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:
|
||||
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"quoteProps": "preserve",
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
@@ -34,4 +34,6 @@
|
||||
- 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)
|
||||
@@ -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
|
||||
13
README.md
13
README.md
@@ -6,6 +6,7 @@ title: Docker Registry User Interface
|
||||
|
||||

|
||||

|
||||
[](https://github.com/sponsors/Joxit)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -19,6 +20,8 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
|
||||

|
||||
|
||||
If you like my work and want to support it, don't hesitate to [sponsor me](https://github.com/sponsors/Joxit).
|
||||
|
||||
## Features
|
||||
|
||||
- List all your repositories/images.
|
||||
@@ -48,6 +51,9 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- Desactivate add and remove regisitries with `READ_ONLY_REGISTRIES` (see [#219](https://github.com/Joxit/docker-registry-ui/pull/219)).
|
||||
- Filter images and tags with a search bar. You can select the search bar with the shortcut `CRTL` + `F` or `F3`. When the search bar is already focused, the shortcut will fallback to the default behavior (see [#213](https://github.com/Joxit/docker-registry-ui/issues/213)).
|
||||
- **Forward** custom header to your backend registry via environment variable and file via `NGINX_PROXY_PASS_HEADER_*` (see [#206](https://github.com/Joxit/docker-registry-ui/pull/206)).
|
||||
- Run the container with user nginx instead of root via `--user nginx` and listend on custom port via `NGINX_LISTEN_PORT` (see [#224](https://github.com/Joxit/docker-registry-ui/issues/224)).
|
||||
- Show number of tags per images on catalog page. This will produce + nb images requests, not recommended on large registries via `SHOW_CATALOG_NB_TAGS` (default: `false`) (see [#161](https://github.com/Joxit/docker-registry-ui/issues/161) and [#239](https://github.com/Joxit/docker-registry-ui/pull/239)).
|
||||
- Expose custom labels in history page, custom labels will be processed like maintainer label via `HISTORY_CUSTOM_LABELS` (see [#160](https://github.com/Joxit/docker-registry-ui/issues/160) and [#240](https://github.com/Joxit/docker-registry-ui/pull/240)).
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -75,12 +81,16 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- When you delete an image with the UI, only the reference is deleted and not the content. To remove dangling images, you need to run the garbage collector of the registry with the command `registry garbage-collect config.yml` or `docker exec registry registry garbage-collect config.yml`. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77) [#147](https://github.com/Joxit/docker-registry-ui/issues/147))
|
||||
- Why when I delete one tag, all tags with the same SHA are deleted ?
|
||||
- This a docker registry API limitation, there is only one way to [delete images with tag](https://docs.docker.com/registry/spec/api/#deleting-an-image), it's by its `name` and its `manifest` (it's a sha of the content). So when you delete a tag, this will delete all tags of this image with the same SHA/manifest.
|
||||
- Can I run the container with an unprivileged user ?
|
||||
- Yes you can run the container with the `nginx` user, (see [#224](https://github.com/Joxit/docker-registry-ui/issues/224)).
|
||||
|
||||
|
||||
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples) or open an issue.
|
||||
|
||||
## Available options
|
||||
|
||||
You can run the container with the unprivileged user `nginx`, see the discussion [#224](https://github.com/Joxit/docker-registry-ui/issues/224).
|
||||
|
||||
Some env options are available for use this interface for **only one server**.
|
||||
|
||||
- `REGISTRY_URL`: The default url of your docker registry. You may need CORS configuration on your registry. This is usually the domain name or IP of your registry reachable by your computer (e.g `http://registry.example.com`). (default: derived from the hostname of your UI).
|
||||
@@ -93,8 +103,11 @@ Some env options are available for use this interface for **only one server**.
|
||||
- `NGINX_PROXY_PASS_URL`: Update the default Nginx configuration and set the **proxy_pass** to your backend docker registry (this avoid CORS configuration). This is usually the name of your registry container in the form `http://registry:5000`.
|
||||
- `NGINX_PROXY_HEADER_*`: Update the default Nginx configuration and **set custom headers** for your backend docker registry. Only when `NGINX_PROXY_PASS_URL` is used.
|
||||
- `NGINX_PROXY_HEADER_*`: Update the default Nginx configuration and **forward custom headers** to your backend docker registry. Only when `NGINX_PROXY_PASS_URL` is used.
|
||||
- `NGINX_LISTEN_PORT`: Listen on a port other than 80. (default: `80` when the user is root, `8080` otherwise).
|
||||
- `DEFAULT_REGISTRIES`: List of comma separated registry URLs (e.g `http://registry.example.com,http://registry:5000`), available only when `SINGLE_REGISTRY=false`. (default: ` `).
|
||||
- `READ_ONLY_REGISTRIES`: Desactivate dialog for remove and add new registries, available only when `SINGLE_REGISTRY=false`. (default: `false`).
|
||||
- `SHOW_CATALOG_NB_TAGS`: Show number of tags per images on catalog page. This will produce + nb images requests, not recommended on large registries. (default: `false`).
|
||||
- `HISTORY_CUSTOM_LABELS`: Expose custom labels in history page, custom labels will be processed like maintainer label.
|
||||
|
||||
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/).
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,8 @@ 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
|
||||
|
||||
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
|
||||
sed -i "s/\${DELETE_IMAGES}/false/" index.html
|
||||
@@ -54,3 +56,15 @@ if [ -n "${NGINX_PROXY_PASS_URL}" ] ; then
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', 'UA-99119327-1', 'auto');
|
||||
ga('create', 'G-T158HYBVZ2', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
4
dist/docker-registry-ui.js
vendored
4
dist/docker-registry-ui.js
vendored
File diff suppressed because one or more lines are too long
17
dist/index.html
vendored
17
dist/index.html
vendored
@@ -13,7 +13,16 @@
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
--><!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}"></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}"
|
||||
></docker-registry-ui><script src="docker-registry-ui.js"></script></body></html>
|
||||
@@ -1,6 +1,18 @@
|
||||
{{- if .Values.ui.ingress.enabled -}}
|
||||
{{- $fullName := include "docker-registry-ui.fullname" . -}}
|
||||
{{- $svcPort := .Values.ui.service.port -}}
|
||||
{{- if and .Values.ui.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ui.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ui.ingress.annotations "kubernetes.io/ingress.class" .Values.ui.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
@@ -11,6 +23,9 @@ metadata:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ui.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ui.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ui.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ui.ingress.tls }}
|
||||
@@ -26,9 +41,21 @@ spec:
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -41,11 +41,15 @@ ui:
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: "nginx"
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: docker-registry-ui.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
|
||||
@@ -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
|
||||
|
||||
33
package.json
33
package.json
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"name": "docker-registry-ui",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.1",
|
||||
"scripts": {
|
||||
"start": "ROLLUP_SERVE=true rollup -c -w",
|
||||
"format": "npm run format-html && npm run format-js && npm run format-riot",
|
||||
"format-html": "find src rollup rollup.config.js -name '*.html' -exec prettier --config .prettierrc -w --parser html {} \\;",
|
||||
"format-js": "find src rollup rollup.config.js -name '*.js' -exec prettier --config .prettierrc -w {} \\;",
|
||||
"format-riot": "find src rollup rollup.config.js -name '*.riot' -exec prettier --config .prettierrc -w --parser html {} \\;",
|
||||
"start": "rollup -c -w --environment ROLLUP_SERVE:true",
|
||||
"build": "rollup -c",
|
||||
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
|
||||
},
|
||||
@@ -14,28 +18,29 @@
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A web UI for private docker registry",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@riotjs/compiler": "^5.4.2",
|
||||
"@riotjs/compiler": "^6.1.3",
|
||||
"@riotjs/observable": "^4.1.1",
|
||||
"@riotjs/route": "^7.1.2",
|
||||
"@riotjs/route": "^8.0.1",
|
||||
"@rollup/plugin-babel": "^5.2.2",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-commonjs": "^21.1.0",
|
||||
"@rollup/plugin-html": "^0.2.4",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||
"core-js": "^3.19.1",
|
||||
"js-beautify": "^1.14.0",
|
||||
"riot": "^5.4.5",
|
||||
"@rollup/plugin-node-resolve": "^13.2.1",
|
||||
"core-js": "^3.22.0",
|
||||
"node-sass": "^7.0.1",
|
||||
"prettier": "^2.6.2",
|
||||
"riot": "^6.1.2",
|
||||
"riot-mui": "github:joxit/riot-5-mui#4d68d7f",
|
||||
"rollup": "^2.59.0",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup-plugin-app-utils": "^1.0.6",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-riot": "^5.0.0",
|
||||
"rollup-plugin-scss": "^2.6.1",
|
||||
"rollup-plugin-riot": "^6.0.0",
|
||||
"rollup-plugin-scss": "^3.0.0",
|
||||
"rollup-plugin-serve": "^1.1.0",
|
||||
"rollup-plugin-styles": "^3.14.1",
|
||||
"rollup-plugin-styles": "^4.0.0",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import json from '@rollup/plugin-json';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import copyTransform from './rollup/copy-transform';
|
||||
import license from './rollup/license';
|
||||
import checkOutput from './rollup/check-output';
|
||||
|
||||
const useServe = process.env.ROLLUP_SERVE === 'true';
|
||||
const output = useServe ? '.serve' : 'dist';
|
||||
@@ -44,11 +45,12 @@ export default [
|
||||
dir: output,
|
||||
name: 'DockerRegistryUI',
|
||||
format: 'iife',
|
||||
sourcemap: useServe
|
||||
sourcemap: useServe,
|
||||
},
|
||||
plugins: [emptyDirectories(output)].concat(
|
||||
plugins,
|
||||
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) })
|
||||
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) }),
|
||||
checkOutput(output)
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
40
rollup/check-output.js
Normal file
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`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -15,4 +15,4 @@ export default `/*
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @license AGPL
|
||||
*/`
|
||||
*/`;
|
||||
|
||||
@@ -16,11 +16,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<catalog-element>
|
||||
<!-- Begin of tag -->
|
||||
<div class="content"
|
||||
if="{!props.filterResults || state.nImages > 0 || matchSearch(props.filterResults, state.image)}">
|
||||
<div
|
||||
class="content"
|
||||
if="{!props.filterResults || state.nImages > 0 || matchSearch(props.filterResults, state.image)}"
|
||||
>
|
||||
<material-card class="list highlight" expanded="{state.expanded}" onclick="{ onClick }">
|
||||
<material-waves onmousedown="{this.triggerLaunch}" center="true" color="#ddd"
|
||||
setLaunchListener="{ setLaunchListener }" />
|
||||
<material-waves center="true" color="#ddd"></material-waves>
|
||||
<span>
|
||||
<i class="material-icons">send</i>
|
||||
{ state.image || state.repo }
|
||||
@@ -28,17 +29,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
{ state.nImages } images
|
||||
<i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i>
|
||||
</div>
|
||||
<div if="{props.showCatalogNbTags && state.image}" class="item-count right">
|
||||
{ state.nbTags } tags
|
||||
<i class="material-icons animated"></i>
|
||||
</div>
|
||||
</span>
|
||||
</material-card>
|
||||
<catalog-element if="{ state.images }" filter-results="{ props.filterResults }"
|
||||
<catalog-element
|
||||
if="{ state.images }"
|
||||
filter-results="{ props.filterResults }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
on-notify="{ props.onnNotify }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
show-catalog-nb-tags="{ props.showCatalogNbTags }"
|
||||
class="animated {!state.expanded && !props.filterResults ? 'hide' : ''} {state.expanding ? 'expanding' : ''}"
|
||||
each="{item in state.images}" item="{ item }" />
|
||||
each="{item in state.images}"
|
||||
item="{ item }"
|
||||
></catalog-element>
|
||||
</div>
|
||||
<script>
|
||||
import router from '../../scripts/router';
|
||||
import {
|
||||
matchSearch
|
||||
} from '../search-bar.riot';
|
||||
import { Http } from '../../scripts/http';
|
||||
import { matchSearch } from '../search-bar.riot';
|
||||
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
@@ -51,10 +63,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
state.repo = props.item.repo;
|
||||
state.nImages = props.item.images.length;
|
||||
}
|
||||
if (props.showCatalogNbTags && state.image) {
|
||||
this.getNbTags(props, state);
|
||||
}
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
if (props.filterResults && state.images) {
|
||||
state.nImages = state.images.filter(image => matchSearch(props.filterResults, image)).length;
|
||||
state.nImages = state.images.filter((image) => matchSearch(props.filterResults, image)).length;
|
||||
} else {
|
||||
state.nImages = state.images && state.images.length;
|
||||
}
|
||||
@@ -66,20 +81,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
} else {
|
||||
this.update({
|
||||
expanded: !this.state.expanded,
|
||||
expanding: true
|
||||
expanding: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.update({
|
||||
expanding: false
|
||||
expanding: false,
|
||||
});
|
||||
}, 50)
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
setLaunchListener(cb) {
|
||||
this.triggerLaunch = cb;
|
||||
getNbTags(props, state) {
|
||||
const self = this;
|
||||
const oReq = new Http({
|
||||
onAuthentication: props.onAuthentication,
|
||||
});
|
||||
oReq.addEventListener('load', function () {
|
||||
if (this.status === 200) {
|
||||
const nbTags = (JSON.parse(this.responseText).tags || []).length;
|
||||
self.update({ nbTags });
|
||||
} else if (this.status === 404) {
|
||||
props.onNotify('Server not found', true);
|
||||
} else {
|
||||
props.onNotify(this.responseText, true);
|
||||
}
|
||||
});
|
||||
oReq.addEventListener('error', function () {
|
||||
props.onNotify(this.getErrorMessage(), true);
|
||||
});
|
||||
oReq.open('GET', props.registryUrl + '/v2/' + state.image + '/tags/list');
|
||||
oReq.send();
|
||||
},
|
||||
matchSearch
|
||||
}
|
||||
matchSearch,
|
||||
};
|
||||
</script>
|
||||
<!-- End of tag -->
|
||||
</catalog-element>
|
||||
</catalog-element>
|
||||
|
||||
@@ -26,26 +26,30 @@ 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 {
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
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: [],
|
||||
registryUrl: ''
|
||||
registryUrl: '',
|
||||
},
|
||||
|
||||
onBeforeMount(props) {
|
||||
@@ -53,7 +57,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
this.state.catalogElementsLimit = props.catalogElementsLimit;
|
||||
},
|
||||
onMounted(props, state) {
|
||||
this.display(props, state)
|
||||
this.display(props, state);
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
this.display(props, state);
|
||||
@@ -66,20 +70,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
let repositories = [];
|
||||
const self = this;
|
||||
const oReq = new Http({
|
||||
onAuthentication: this.props.onAuthentication
|
||||
onAuthentication: this.props.onAuthentication,
|
||||
});
|
||||
oReq.addEventListener('load', function () {
|
||||
if (this.status == 200) {
|
||||
if (this.status === 200) {
|
||||
repositories = JSON.parse(this.responseText).repositories || [];
|
||||
repositories.sort();
|
||||
repositories = repositories.reduce(function (acc, e) {
|
||||
const slash = e.indexOf('/');
|
||||
if (slash > 0) {
|
||||
const repoName = e.substring(0, slash) + '/';
|
||||
if (acc.length == 0 || acc[acc.length - 1].repo != repoName) {
|
||||
if (acc.length === 0 || acc[acc.length - 1].repo != repoName) {
|
||||
acc.push({
|
||||
repo: repoName,
|
||||
images: []
|
||||
images: [],
|
||||
});
|
||||
}
|
||||
acc[acc.length - 1].images.push(e);
|
||||
@@ -88,7 +92,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
acc.push(e);
|
||||
return acc;
|
||||
}, []);
|
||||
} else if (this.status == 404) {
|
||||
} else if (this.status === 404) {
|
||||
self.props.onNotify('Server not found', true);
|
||||
} else {
|
||||
self.props.onNotify(this.responseText);
|
||||
@@ -101,13 +105,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
self.update({
|
||||
repositories,
|
||||
nRepositories: repositories.length,
|
||||
nImages: repositories.reduce((acc, e) => acc + (e.images && e.images.length || 1), 0),
|
||||
loadend: true
|
||||
nImages: repositories.reduce((acc, e) => acc + ((e.images && e.images.length) || 1), 0),
|
||||
loadend: true,
|
||||
});
|
||||
});
|
||||
oReq.open('GET', `${props.registryUrl}/v2/_catalog?n=${state.catalogElementsLimit}`);
|
||||
oReq.send();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</catalog>
|
||||
</catalog>
|
||||
|
||||
@@ -31,9 +31,7 @@
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
addRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import { addRegistryServers } from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
|
||||
export default {
|
||||
@@ -52,11 +50,11 @@
|
||||
return this.props.onNotify('The input field should start with http:// or https://.', true);
|
||||
}
|
||||
const url = addRegistryServers(input.value);
|
||||
router.home()
|
||||
router.home();
|
||||
this.props.onServerChange(url);
|
||||
this.props.onClose()
|
||||
this.props.onClose();
|
||||
setTimeout(() => router.updateUrlQueryParam(url), 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</add-registry-url>
|
||||
</add-registry-url>
|
||||
|
||||
@@ -32,10 +32,7 @@
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
addRegistryServers,
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import { addRegistryServers, getRegistryServers } from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
change(event) {
|
||||
@@ -47,13 +44,13 @@
|
||||
return this.props.onNotify('The select field should start with http:// or https://.', true);
|
||||
}
|
||||
const url = addRegistryServers(select.value);
|
||||
router.home()
|
||||
router.home();
|
||||
this.props.onServerChange(url);
|
||||
this.props.onClose()
|
||||
this.props.onClose();
|
||||
setTimeout(() => router.updateUrlQueryParam(url), 100);
|
||||
},
|
||||
getRegistryServers
|
||||
}
|
||||
getRegistryServers,
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host select {
|
||||
@@ -74,4 +71,4 @@
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
</style>
|
||||
</change-registry-url>
|
||||
</change-registry-url>
|
||||
|
||||
@@ -32,66 +32,80 @@
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import { Http } from '../../scripts/http';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
displayImagesToDelete(toDelete, tags) {
|
||||
const digests = new Set();
|
||||
toDelete.forEach(image => {
|
||||
toDelete.forEach((image) => {
|
||||
if (image.digest) {
|
||||
digests.add(image.digest);
|
||||
}
|
||||
})
|
||||
return tags.filter(image => digests.has(image.digest))
|
||||
});
|
||||
return tags.filter((image) => digests.has(image.digest));
|
||||
},
|
||||
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 (digest) {
|
||||
if (!digest) {
|
||||
onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
|
||||
} else {
|
||||
self.deleteImage({ name, tag, digest }, 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, digest }, 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/${digest}`);
|
||||
oReq.setRequestHeader(
|
||||
'Accept',
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json'
|
||||
);
|
||||
oReq.addEventListener('error', function () {
|
||||
onNotify({
|
||||
message: 'An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].',
|
||||
isError: true
|
||||
message:
|
||||
"An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: ['DELETE'].",
|
||||
isError: true,
|
||||
});
|
||||
});
|
||||
oReq.send();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host {
|
||||
@@ -105,4 +119,4 @@
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
</confirm-delete-image>
|
||||
</confirm-delete-image>
|
||||
|
||||
@@ -15,18 +15,35 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<dialogs-menu>
|
||||
<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>
|
||||
<add-registry-url
|
||||
if="{ !props.readOnlyRegistries }"
|
||||
opened="{ state['add-registry-url'] }"
|
||||
on-close="{ onClose('add-registry-url') }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-server-change="{ props.onServerChange }"
|
||||
></add-registry-url>
|
||||
<change-registry-url
|
||||
opened="{ state['change-registry-url'] }"
|
||||
on-close="{ onClose('change-registry-url') }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-server-change="{ props.onServerChange }"
|
||||
></change-registry-url>
|
||||
<remove-registry-url
|
||||
if="{ !props.readOnlyRegistries }"
|
||||
opened="{ state['remove-registry-url'] }"
|
||||
on-close="{ onClose('remove-registry-url') }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-server-change="{ props.onServerChange }"
|
||||
></remove-registry-url>
|
||||
<div class="container">
|
||||
<material-button onClick="{ onClick }" waves-center="true" rounded="true" waves-opacity="0.6" waves-duration="600">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</material-button>
|
||||
<material-dropdown-list items="{ dropdownItems.filter(item => item.ro || !props.readOnlyRegistries) }" onSelect="{ onDropdownSelect }"
|
||||
opened="{ state.isDropdownOpened }" />
|
||||
<material-dropdown-list
|
||||
items="{ dropdownItems.filter(item => item.ro || !props.readOnlyRegistries) }"
|
||||
onSelect="{ onDropdownSelect }"
|
||||
opened="{ state.isDropdownOpened }"
|
||||
/>
|
||||
</div>
|
||||
<div class="overlay" onclick="{ onClick }" if="{ state.isDropdownOpened }"></div>
|
||||
<script>
|
||||
@@ -38,44 +55,48 @@
|
||||
components: {
|
||||
AddRegistryUrl,
|
||||
ChangeRegistryUrl,
|
||||
RemoveRegistryUrl
|
||||
RemoveRegistryUrl,
|
||||
},
|
||||
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
|
||||
}],
|
||||
dropdownItems: [
|
||||
{
|
||||
title: 'Add URL',
|
||||
name: 'add-registry-url',
|
||||
ro: false,
|
||||
},
|
||||
{
|
||||
title: 'Change URL',
|
||||
name: 'change-registry-url',
|
||||
ro: true,
|
||||
},
|
||||
{
|
||||
title: 'Remove URL',
|
||||
name: 'remove-registry-url',
|
||||
ro: false,
|
||||
},
|
||||
],
|
||||
onDropdownSelect(key, item) {
|
||||
this.update({
|
||||
[item.name]: true,
|
||||
isDropdownOpened: false
|
||||
isDropdownOpened: false,
|
||||
});
|
||||
},
|
||||
onClose(name) {
|
||||
return () => {
|
||||
this.update({
|
||||
[name]: false,
|
||||
isDropdownOpened: false
|
||||
})
|
||||
}
|
||||
isDropdownOpened: false,
|
||||
});
|
||||
};
|
||||
},
|
||||
onClick() {
|
||||
this.update({
|
||||
isDropdownOpened: !this.state.isDropdownOpened
|
||||
})
|
||||
}
|
||||
}
|
||||
isDropdownOpened: !this.state.isDropdownOpened,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host > .container{
|
||||
:host > .container {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 16px;
|
||||
@@ -128,4 +149,4 @@
|
||||
line-height: 36px;
|
||||
}
|
||||
</style>
|
||||
</dialogs-menu>
|
||||
</dialogs-menu>
|
||||
|
||||
@@ -21,8 +21,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<ul class="list">
|
||||
<li each="{ url in getRegistryServers() }">
|
||||
<span>
|
||||
<material-button onClick="{ remove }" url="{ url }" rounded="true" waves-color="rgba(158,158,158,.4)"
|
||||
waves-center="true">
|
||||
<material-button
|
||||
onClick="{ remove }"
|
||||
url="{ url }"
|
||||
rounded="true"
|
||||
waves-color="rgba(158,158,158,.4)"
|
||||
waves-center="true"
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<span class="url">{ url }</span>
|
||||
@@ -37,18 +42,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
getRegistryServers,
|
||||
removeRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import { getRegistryServers, removeRegistryServers } from '../../scripts/utils';
|
||||
export default {
|
||||
remove(event) {
|
||||
const url = event.currentTarget.attributes.url && event.currentTarget.attributes.url.value;
|
||||
removeRegistryServers(url);
|
||||
setTimeout(() => this.update(), 100);
|
||||
},
|
||||
getRegistryServers
|
||||
}
|
||||
getRegistryServers,
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host material-popup .popup material-button {
|
||||
@@ -59,4 +61,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</remove-registry-url>
|
||||
</remove-registry-url>
|
||||
|
||||
@@ -19,34 +19,63 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<material-navbar>
|
||||
<div class="logo">Docker Registry UI</div>
|
||||
<search-bar on-search="{ onSearch }"></search-bar>
|
||||
<dialogs-menu if="{props.singleRegistry !== 'true'}" on-notify="{ notifySnackbar }"
|
||||
on-server-change="{ onServerChange }" default-registries="{ props.defaultRegistries }"
|
||||
read-only-registries="{ truthy(props.readOnlyRegistries) }"></dialogs-menu>
|
||||
<dialogs-menu
|
||||
if="{props.singleRegistry !== 'true'}"
|
||||
on-notify="{ notifySnackbar }"
|
||||
on-server-change="{ onServerChange }"
|
||||
default-registries="{ props.defaultRegistries }"
|
||||
read-only-registries="{ truthy(props.readOnlyRegistries) }"
|
||||
></dialogs-menu>
|
||||
</material-navbar>
|
||||
</header>
|
||||
<main>
|
||||
<router base="#!">
|
||||
<route path="{baseRoute}">
|
||||
<catalog registry-url="{ state.registryUrl }" registry-name="{ state.name }"
|
||||
catalog-elements-limit="{ state.catalogElementsLimit }" on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }" on-authentication="{ onAuthentication }" />
|
||||
<catalog
|
||||
registry-url="{ state.registryUrl }"
|
||||
registry-name="{ state.name }"
|
||||
catalog-elements-limit="{ state.catalogElementsLimit }"
|
||||
on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }"
|
||||
on-authentication="{ onAuthentication }"
|
||||
show-catalog-nb-tags="{ truthy(props.showCatalogNbTags) }"
|
||||
/>
|
||||
</route>
|
||||
<route path="{baseRoute}taglist/(.*)">
|
||||
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagListImage() }" show-content-digest="{ truthy(props.showContentDigest) }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }" on-authentication="{ onAuthentication }"></tag-list>
|
||||
<tag-list
|
||||
registry-url="{ state.registryUrl }"
|
||||
registry-name="{ state.name }"
|
||||
pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagListImage() }"
|
||||
show-content-digest="{ truthy(props.showContentDigest) }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }"
|
||||
on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }"
|
||||
on-authentication="{ onAuthentication }"
|
||||
></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) }"
|
||||
></tag-history>
|
||||
</route>
|
||||
</router>
|
||||
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
|
||||
on-close="{ onAuthenticationClose }" on-authenticated="{ state.onAuthenticated }"
|
||||
opened="{ state.authenticationDialogOpened }"></registry-authentication>
|
||||
<registry-authentication
|
||||
realm="{ state.realm }"
|
||||
scope="{ state.scope }"
|
||||
service="{ state.service }"
|
||||
on-close="{ onAuthenticationClose }"
|
||||
on-authenticated="{ state.onAuthenticated }"
|
||||
opened="{ state.authenticationDialogOpened }"
|
||||
></registry-authentication>
|
||||
<material-snackbar message="{ state.snackbarMessage }" is-error="{ state.snackbarIsError }"></material-snackbar>
|
||||
</main>
|
||||
<footer>
|
||||
@@ -63,24 +92,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</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,
|
||||
setRegistryServers,
|
||||
truthy
|
||||
} from '../scripts/utils';
|
||||
import SearchBar from './search-bar.riot';
|
||||
import { stripHttps, getRegistryServers, setRegistryServers, truthy, stringToArray } from '../scripts/utils';
|
||||
import router from '../scripts/router';
|
||||
|
||||
export default {
|
||||
@@ -91,23 +110,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
DialogsMenu,
|
||||
SearchBar,
|
||||
Router,
|
||||
Route
|
||||
Route,
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
state.snackbarIsError = false;
|
||||
state.snackbarMessage = undefined;
|
||||
},
|
||||
onBeforeMount(props) {
|
||||
if ((props.defaultRegistries && props.defaultRegistries.length > 0 && getRegistryServers().length === 0) ||
|
||||
truthy(props.readOnlyRegistries)) {
|
||||
if (
|
||||
(props.defaultRegistries && props.defaultRegistries.length > 0 && getRegistryServers().length === 0) ||
|
||||
truthy(props.readOnlyRegistries)
|
||||
) {
|
||||
setRegistryServers(props.defaultRegistries);
|
||||
}
|
||||
|
||||
// props.singleRegistry === 'true' means old static version
|
||||
const registryUrl = props.registryUrl ||
|
||||
(props.singleRegistry === 'true' ? undefined : (router.getUrlQueryParam() || getRegistryServers(0))) ||
|
||||
(window.location.origin + window.location.pathname.replace(/\/+$/, ''));
|
||||
this.state.registryUrl = registryUrl.replace(/\/$/, '');
|
||||
// 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);
|
||||
@@ -117,64 +139,59 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
registryUrl,
|
||||
name: stripHttps(registryUrl),
|
||||
pullUrl: this.pullUrl(registryUrl),
|
||||
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.'
|
||||
})
|
||||
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.',
|
||||
});
|
||||
},
|
||||
onAuthentication(opts, onAuthenticated) {
|
||||
if (opts && opts.realm && opts.service && opts.scope) {
|
||||
const {
|
||||
realm,
|
||||
service,
|
||||
scope,
|
||||
} = opts;
|
||||
const req = new XMLHttpRequest()
|
||||
const { realm, service, scope } = opts;
|
||||
const req = new XMLHttpRequest();
|
||||
req.addEventListener('loadend', () => {
|
||||
try {
|
||||
const bearer = JSON.parse(req.responseText);
|
||||
onAuthenticated(bearer)
|
||||
onAuthenticated(bearer);
|
||||
} catch (e) {
|
||||
this.notifySnackbar(`Failed to log in: ${e.message}`, true)
|
||||
this.notifySnackbar(`Failed to log in: ${e.message}`, true);
|
||||
}
|
||||
})
|
||||
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
|
||||
req.send()
|
||||
});
|
||||
req.open('GET', `${realm}?service=${service}&scope=${scope}`);
|
||||
req.send();
|
||||
} else {
|
||||
onAuthenticated()
|
||||
onAuthenticated();
|
||||
}
|
||||
},
|
||||
onAuthenticationClose() {
|
||||
this.update({
|
||||
authenticationDialogOpened: false
|
||||
})
|
||||
authenticationDialogOpened: false,
|
||||
});
|
||||
},
|
||||
pullUrl(registryUrl, pullUrl) {
|
||||
const url = pullUrl ||
|
||||
(registryUrl && registryUrl.length > 0 && registryUrl) ||
|
||||
window.location.host;
|
||||
const url = pullUrl || (registryUrl && registryUrl.length > 0 && registryUrl) || window.location.host;
|
||||
return stripHttps(url);
|
||||
},
|
||||
notifySnackbar(message, isError) {
|
||||
if (typeof message === 'string') {
|
||||
this.update({
|
||||
snackbarMessage: message,
|
||||
snackbarIsError: isError || false
|
||||
snackbarIsError: isError || false,
|
||||
});
|
||||
} else if (message && message.message) {
|
||||
this.update({
|
||||
snackbarMessage: message.message,
|
||||
snackbarIsError: message.isError
|
||||
snackbarIsError: message.isError,
|
||||
});
|
||||
}
|
||||
},
|
||||
onSearch(value) {
|
||||
this.update({
|
||||
filter: value
|
||||
})
|
||||
filter: value,
|
||||
});
|
||||
},
|
||||
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
|
||||
router,
|
||||
version,
|
||||
truthy
|
||||
}
|
||||
truthy,
|
||||
stringToArray,
|
||||
};
|
||||
</script>
|
||||
</docker-registry-ui>
|
||||
</docker-registry-ui>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<search-bar>
|
||||
<material-input placeholder="Search in page"></material-input>
|
||||
<script>
|
||||
import {
|
||||
router
|
||||
} from '@riotjs/route';
|
||||
import { router } from '@riotjs/route';
|
||||
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
@@ -11,16 +9,16 @@
|
||||
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 => {
|
||||
});
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// F3 or CTRL + F
|
||||
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
|
||||
// already focused, fallback to default behavior
|
||||
@@ -31,9 +29,9 @@
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function matchSearch(search, value) {
|
||||
return !search || (value && value.toLowerCase().indexOf(search) >= 0);
|
||||
@@ -57,4 +55,4 @@
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</search-bar>
|
||||
</search-bar>
|
||||
|
||||
@@ -15,17 +15,16 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-history-element class="{ state.key }">
|
||||
<div class="headline"><i class="material-icons">{ state.icon }</i>
|
||||
<div class="headline">
|
||||
<i class="material-icons">{ state.icon }</i>
|
||||
<p>{ state.name }</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="value" if="{ state.value }"> { state.value }</div>
|
||||
<div class="values value" each="{ value in state.values }" if="{ state.values }"> { value }</div>
|
||||
<div class="value" if="{ state.value }">{ state.value }</div>
|
||||
<div class="values value" each="{ value in state.values }" if="{ state.values }">{ value }</div>
|
||||
</div>
|
||||
<script>
|
||||
import {
|
||||
getHistoryIcon
|
||||
} from '../../scripts/utils';
|
||||
import { getHistoryIcon } from '../../scripts/utils';
|
||||
export default {
|
||||
onBeforeStart(props, state) {
|
||||
state.key = props.entry.key;
|
||||
@@ -48,14 +47,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return name;
|
||||
} else if (name === 'os') {
|
||||
return 'OS';
|
||||
} else if (name.startsWith('custom-label-')) {
|
||||
name = name.replace('custom-label-', '');
|
||||
}
|
||||
return name.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace('_', ' ')
|
||||
return name
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
||||
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
:host.Labels .value,
|
||||
@@ -70,7 +72,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
:host.docker_version .headline .material-icons {
|
||||
background-size: 24px auto;
|
||||
background-image: url("images/docker-logo.svg");
|
||||
background-image: url('images/docker-logo.svg');
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@@ -103,4 +105,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</tag-history-element>
|
||||
</tag-history-element>
|
||||
|
||||
@@ -20,33 +20,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ toTaglist }">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
History of { props.image }:{ props.tag } <i class="material-icons">history</i>
|
||||
</h2>
|
||||
<h2>History of { props.image }:{ props.tag } <i class="material-icons">history</i></h2>
|
||||
</div>
|
||||
</material-card>
|
||||
<div if="{ !state.loadend }" class="spinner-wrapper">
|
||||
<material-spinner />
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
|
||||
<material-tabs if="{ state.archs && state.loadend }" useLine="{ true }" tabs="{ state.archs }"
|
||||
onTabChanged="{ onTabChanged }" />
|
||||
<material-tabs
|
||||
if="{ state.archs && state.loadend }"
|
||||
useLine="{ true }"
|
||||
tabs="{ state.archs }"
|
||||
onTabChanged="{ onTabChanged }"
|
||||
></material-tabs>
|
||||
|
||||
<material-card each="{ element in state.elements }" class="tag-history-element">
|
||||
<tag-history-element each="{ entry in element }" if="{ entry.value && entry.value.length > 0}" entry="{ entry }" />
|
||||
<tag-history-element
|
||||
each="{ entry in element }"
|
||||
if="{ entry.value && entry.value.length > 0}"
|
||||
entry="{ entry }"
|
||||
></tag-history-element>
|
||||
</material-card>
|
||||
<script>
|
||||
import {
|
||||
DockerImage
|
||||
} from '../../scripts/docker-image';
|
||||
import {
|
||||
bytesToSize
|
||||
} from '../../scripts/utils';
|
||||
import { DockerImage } from '../../scripts/docker-image';
|
||||
import { bytesToSize } from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
import TagHistoryElement from './tag-history-element.riot'
|
||||
import TagHistoryElement from './tag-history-element.riot';
|
||||
export default {
|
||||
components: {
|
||||
TagHistoryElement
|
||||
TagHistoryElement,
|
||||
},
|
||||
onBeforeMount(props, state) {
|
||||
state.elements = [];
|
||||
@@ -54,9 +56,9 @@ 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,
|
||||
});
|
||||
state.image.fillInfo()
|
||||
state.image.fillInfo();
|
||||
},
|
||||
onMounted(props, state) {
|
||||
state.image.on('blobs', this.processBlobs);
|
||||
@@ -64,16 +66,14 @@ 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 } = this.props;
|
||||
state.elements = [];
|
||||
state.image.variants[idx] =
|
||||
state.image.variants[idx] ||
|
||||
new DockerImage(this.props.image, arch.digest, {
|
||||
list: false,
|
||||
registryUrl,
|
||||
onNotify
|
||||
onNotify,
|
||||
});
|
||||
if (state.image.variants[idx].blobs) {
|
||||
return this.processBlobs(state.image.variants[idx].blobs);
|
||||
@@ -83,6 +83,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
},
|
||||
processBlobs(blobs) {
|
||||
const state = this.state;
|
||||
const { historyCustomLabels } = this.props;
|
||||
|
||||
function exec(elt) {
|
||||
const guiElements = [];
|
||||
@@ -90,8 +91,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
|
||||
const value = elt[attribute];
|
||||
const guiElement = {
|
||||
"key": attribute,
|
||||
"value": modifySpecificAttributeTypes(attribute, value)
|
||||
'key': attribute,
|
||||
'value': modifySpecificAttributeTypes(attribute, value),
|
||||
};
|
||||
guiElements.push(guiElement);
|
||||
}
|
||||
@@ -99,32 +100,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return guiElements.sort(eltSort);
|
||||
}
|
||||
const elements = new Array(blobs.history.length + 1);
|
||||
elements[0] = exec(getConfig(blobs));
|
||||
elements[0] = exec(getConfig(blobs, { historyCustomLabels }));
|
||||
blobs.history.forEach(function (elt, i) {
|
||||
elements[blobs.history.length - i] = exec(elt)
|
||||
elements[blobs.history.length - i] = exec(elt);
|
||||
});
|
||||
this.update({
|
||||
elements,
|
||||
loadend: true
|
||||
loadend: true,
|
||||
});
|
||||
},
|
||||
multiArchList(manifests) {
|
||||
manifests = manifests.manifests || manifests;
|
||||
const archs = manifests.map(function (manifest) {
|
||||
return {
|
||||
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ?
|
||||
manifest.platform.variant : ''),
|
||||
digest: manifest.digest
|
||||
}
|
||||
title:
|
||||
manifest.platform.os +
|
||||
'/' +
|
||||
manifest.platform.architecture +
|
||||
(manifest.platform.variant ? manifest.platform.variant : ''),
|
||||
digest: manifest.digest,
|
||||
};
|
||||
});
|
||||
this.update({
|
||||
archs
|
||||
archs,
|
||||
});
|
||||
},
|
||||
toTaglist() {
|
||||
router.taglist(this.props.image);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
const eltIdx = function (e) {
|
||||
switch (e) {
|
||||
case 'created':
|
||||
@@ -158,7 +162,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return new Date(value).toLocaleString();
|
||||
case 'created_by':
|
||||
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
|
||||
return (cmd && cmd[1]) || 'RUN'
|
||||
return (cmd && cmd[1]) || 'RUN';
|
||||
case 'size':
|
||||
return bytesToSize(value);
|
||||
case 'Entrypoint':
|
||||
@@ -175,26 +179,48 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return value || '';
|
||||
};
|
||||
|
||||
const getConfig = function (blobs) {
|
||||
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels',
|
||||
'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts'
|
||||
]
|
||||
.reduce(function (acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value && e === 'architecture' && blobs.variant) {
|
||||
acc[e] = value + blobs.variant;
|
||||
} else if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const getConfig = function (blobs, { historyCustomLabels }) {
|
||||
const res = [
|
||||
'architecture',
|
||||
'User',
|
||||
'created',
|
||||
'docker_version',
|
||||
'os',
|
||||
'Cmd',
|
||||
'Entrypoint',
|
||||
'Env',
|
||||
'Labels',
|
||||
'User',
|
||||
'Volumes',
|
||||
'WorkingDir',
|
||||
'author',
|
||||
'id',
|
||||
'ExposedPorts',
|
||||
].reduce(function (acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value && e === 'architecture' && blobs.variant) {
|
||||
acc[e] = value + blobs.variant;
|
||||
} else if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!res.author && (res.Labels && res.Labels.maintainer)) {
|
||||
if (!res.author && res.Labels && res.Labels.maintainer) {
|
||||
res.author = blobs.config.Labels.maintainer;
|
||||
delete res.Labels.maintainer;
|
||||
}
|
||||
|
||||
if (res.Labels) {
|
||||
historyCustomLabels
|
||||
.filter((label) => res.Labels[label])
|
||||
.forEach((label) => {
|
||||
res[`custom-label-${label}`] = res.Labels[label];
|
||||
delete res.Labels[label];
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
</script>
|
||||
</tag-history>
|
||||
</tag-history>
|
||||
|
||||
@@ -16,16 +16,19 @@
|
||||
-->
|
||||
<copy-to-clipboard>
|
||||
<div class="copy-to-clipboard">
|
||||
<input style="display: none; width: 1px; height: 1px;" value="{ getDockerCmd(props) }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ copy }"
|
||||
title="Copy pull command.">
|
||||
<input style="display: none; width: 1px; height: 1px" value="{ getDockerCmd(props) }" />
|
||||
<material-button
|
||||
waves-center="true"
|
||||
rounded="true"
|
||||
waves-color="#ddd"
|
||||
onClick="{ copy }"
|
||||
title="Copy pull command."
|
||||
>
|
||||
<i class="material-icons">content_copy</i>
|
||||
</material-button>
|
||||
</div>
|
||||
<script>
|
||||
import {
|
||||
ERROR_CAN_NOT_READ_CONTENT_DIGEST
|
||||
} from '../../scripts/utils';
|
||||
import { ERROR_CAN_NOT_READ_CONTENT_DIGEST } from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
this.load(props, state);
|
||||
@@ -37,13 +40,13 @@
|
||||
if (props.target === 'tag') {
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}:${props.image.tag}`;
|
||||
} else {
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.digest}`
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.digest}`;
|
||||
}
|
||||
},
|
||||
load(props, state) {
|
||||
if (props.target !== 'tag' && !props.image.digest) {
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.update()
|
||||
this.update();
|
||||
});
|
||||
props.image.trigger('get-content-digest');
|
||||
}
|
||||
@@ -60,8 +63,8 @@
|
||||
document.execCommand('copy');
|
||||
copyText.style.display = 'none';
|
||||
|
||||
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.')
|
||||
}
|
||||
}
|
||||
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</copy-to-clipboard>
|
||||
</copy-to-clipboard>
|
||||
|
||||
@@ -39,12 +39,12 @@ 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.digest || '';
|
||||
},
|
||||
getDigest(image, chars) {
|
||||
if (chars >= 70) {
|
||||
@@ -54,7 +54,7 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
|
||||
} else {
|
||||
return image.digest && image.digest.slice(0, chars) + '...';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</image-content-digest>
|
||||
</image-content-digest>
|
||||
|
||||
@@ -15,27 +15,27 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<image-date>
|
||||
<div title="Creation date { getLocalDate(props.image) }">{ getDate(props.image) } ago</div>
|
||||
<div title="Creation date { getLocalDate(props.image) }">{ getDate(props.image) }</div>
|
||||
<script>
|
||||
import {
|
||||
dateFormat,
|
||||
} from '../../scripts/utils';
|
||||
import { dateFormat } from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props) {
|
||||
props.image.one('creation-date', (date) => {
|
||||
this.update({
|
||||
date: date,
|
||||
localDate: date.toLocaleString()
|
||||
localDate: date && date.toLocaleString(),
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-date');
|
||||
},
|
||||
getDate(image) {
|
||||
return dateFormat(image.creationDate)
|
||||
return !image.ociImage ? `${dateFormat(image.creationDate)} ago` : 'Not Available';
|
||||
},
|
||||
getLocalDate(image) {
|
||||
return (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
|
||||
}
|
||||
}
|
||||
return !image.ociImage
|
||||
? (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
|
||||
: 'unavailable on OCI index/Buildkit export cache';
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</image-date>
|
||||
</image-date>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,14 +17,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<pagination>
|
||||
<div class="conatianer">
|
||||
<div class="pagination-centered">
|
||||
<material-button waves-color="rgba(158,158,158,.4)" each="{p in props.pages}"
|
||||
<material-button
|
||||
waves-color="rgba(158,158,158,.4)"
|
||||
each="{p in props.pages}"
|
||||
class="{ p.current ? 'current' : ''} { p['space-left'] ? 'space-left' : '' } { p['space-right'] ? 'space-right' : ''}"
|
||||
onClick="{() => props.onPageUpdate(p.page)}">
|
||||
onClick="{() => props.onPageUpdate(p.page)}"
|
||||
>
|
||||
<i if="{ p.icon }" class="material-icons">{ p.icon }</i>
|
||||
<div if="{ !p.icon }">{ p.page }</div>
|
||||
</material-button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</pagination>
|
||||
<script></script>
|
||||
</pagination>
|
||||
|
||||
@@ -15,29 +15,35 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<remove-image>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image."
|
||||
if="{ !props.multiDelete }" disabled="{ !state.digest }" onClick="{ deleteImage }">
|
||||
<material-button
|
||||
waves-center="true"
|
||||
rounded="true"
|
||||
waves-color="#ddd"
|
||||
title="This will delete the image."
|
||||
if="{ !props.multiDelete }"
|
||||
disabled="{ !state.digest }"
|
||||
onClick="{ deleteImage }"
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<material-checkbox if="{ props.multiDelete }" title="Select this tag to delete it." disabled="{ !state.digest }"
|
||||
onChange="{ handleCheckboxChange }" checked="{ state.checked }">
|
||||
<material-checkbox
|
||||
if="{ props.multiDelete }"
|
||||
title="Select this tag to delete it."
|
||||
disabled="{ !state.digest }"
|
||||
onChange="{ handleCheckboxChange }"
|
||||
checked="{ state.checked }"
|
||||
>
|
||||
</material-checkbox>
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import router from '../../scripts/router'
|
||||
import {
|
||||
ACTION_CHECK_TO_DELETE,
|
||||
ACTION_UNCHECK_TO_DELETE,
|
||||
ACTION_DELETE_IMAGE
|
||||
} from './tag-table.riot';
|
||||
import { Http } from '../../scripts/http';
|
||||
import router from '../../scripts/router';
|
||||
import { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE } from './tag-table.riot';
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
state.checked = props.checked;
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.update({
|
||||
digest
|
||||
digest,
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -53,7 +59,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
handleCheckboxChange(checked) {
|
||||
const action = checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
|
||||
this.props.handleCheckboxChange(action, this.props.image);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</remove-image>
|
||||
</remove-image>
|
||||
|
||||
@@ -15,16 +15,34 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-history-button>
|
||||
<material-button title="This will show the history of given tag" waves-center="true" rounded="true" waves-color="#ddd"
|
||||
onClick="{ routeToHistory }">
|
||||
<material-button
|
||||
title="{ buttonTittle() }"
|
||||
waves-center="true"
|
||||
rounded="true"
|
||||
waves-color="#ddd"
|
||||
onClick="{ routeToHistory }"
|
||||
disabled="{ props.image.ociImage }"
|
||||
>
|
||||
<i class="material-icons">history</i>
|
||||
</material-button>
|
||||
<script>
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
onMounted(props) {
|
||||
props.image.one('oci-image', () => {
|
||||
this.update();
|
||||
});
|
||||
},
|
||||
buttonTittle() {
|
||||
return !this.props.image.ociImage
|
||||
? 'This will show the history of given tag'
|
||||
: 'History is unavailable on OCI index/Buildkit export cache';
|
||||
},
|
||||
routeToHistory() {
|
||||
router.history(this.props.image.name, this.props.image.tag)
|
||||
}
|
||||
}
|
||||
if (!this.props.image.ociImage) {
|
||||
router.history(this.props.image.name, this.props.image.tag);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</tag-history-button>
|
||||
</tag-history-button>
|
||||
|
||||
@@ -16,15 +16,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-list>
|
||||
<material-card class="header">
|
||||
<div class="material-card-title-action ">
|
||||
<div class="material-card-title-action">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ router.home }">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
Tags of { props.image }
|
||||
<div class="source-hint">
|
||||
Sourced from { state.registryName + '/' + props.image }
|
||||
</div>
|
||||
<div class="source-hint">Sourced from { state.registryName + '/' + props.image }</div>
|
||||
<div class="item-count">{ state.tags.length } tags</div>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -36,30 +34,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||
|
||||
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
|
||||
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
|
||||
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"
|
||||
on-authentication="{ props.onAuthentication }">
|
||||
<tag-table
|
||||
if="{ state.loadend }"
|
||||
tags="{state.tags}"
|
||||
asc="{state.asc}"
|
||||
page="{ state.page }"
|
||||
show-content-digest="{props.showContentDigest}"
|
||||
is-image-remove-activated="{props.isImageRemoveActivated}"
|
||||
onReverseOrder="{ onReverseOrder }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
filter-results="{ props.filterResults }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
>
|
||||
</tag-table>
|
||||
|
||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import {
|
||||
DockerImage,
|
||||
compare
|
||||
} from '../../scripts/docker-image';
|
||||
import {
|
||||
getNumPages,
|
||||
getPageLabels
|
||||
} from '../../scripts/utils'
|
||||
import Pagination from './pagination.riot'
|
||||
import TagTable from './tag-table.riot'
|
||||
import router from '../../scripts/router'
|
||||
import { Http } from '../../scripts/http';
|
||||
import { DockerImage, compare } from '../../scripts/docker-image';
|
||||
import { getNumPages, getPageLabels } from '../../scripts/utils';
|
||||
import Pagination from './pagination.riot';
|
||||
import TagTable from './tag-table.riot';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
components: {
|
||||
Pagination,
|
||||
@@ -71,11 +70,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
tags: [],
|
||||
loadend: false,
|
||||
asc: true,
|
||||
page: router.getPageQueryParam() || 1
|
||||
}
|
||||
page: router.getPageQueryParam() || 1,
|
||||
};
|
||||
},
|
||||
onMounted(props, state) {
|
||||
this.display(props, state)
|
||||
this.display(props, state);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
// this may be run before the final document size is available, so schedule
|
||||
// a correction once everything is set up.
|
||||
@@ -85,23 +84,26 @@ 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, {
|
||||
registryUrl: props.registryUrl,
|
||||
onNotify: props.onNotify,
|
||||
onAuthentication: props.onAuthentication,
|
||||
})
|
||||
)
|
||||
.sort(compare);
|
||||
window.requestAnimationFrame(self.onResize);
|
||||
self.update({
|
||||
page: Math.min(state.page, getNumPages(tags)),
|
||||
tags
|
||||
})
|
||||
} else if (this.status == 404) {
|
||||
tags,
|
||||
});
|
||||
} else if (this.status === 404) {
|
||||
self.props.onNotify('Server not found', true);
|
||||
} else {
|
||||
self.props.onNotify(this.responseText, true);
|
||||
@@ -113,7 +115,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
});
|
||||
oReq.addEventListener('loadend', function () {
|
||||
self.update({
|
||||
loadend: true
|
||||
loadend: true,
|
||||
});
|
||||
});
|
||||
oReq.open('GET', props.registryUrl + '/v2/' + props.image + '/tags/list');
|
||||
@@ -123,7 +125,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
onPageUpdate(page) {
|
||||
this.update({
|
||||
page: page
|
||||
page: page,
|
||||
});
|
||||
router.updatePageQueryParam(page);
|
||||
},
|
||||
@@ -143,8 +145,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 +164,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
},
|
||||
getPageLabels,
|
||||
getNumPages,
|
||||
router
|
||||
}
|
||||
router,
|
||||
};
|
||||
</script>
|
||||
</tag-list>
|
||||
</tag-list>
|
||||
|
||||
@@ -15,39 +15,61 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<tag-table>
|
||||
<confirm-delete-image opened="{ state.confirmDeleteImage }" on-click="{ onConfirmDeleteImageClick }"
|
||||
registry-url="{ props.registryUrl }" on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }"
|
||||
tags="{ props.tags }" to-delete="{ state.toDelete }"></confirm-delete-image>
|
||||
<confirm-delete-image
|
||||
opened="{ state.confirmDeleteImage }"
|
||||
on-click="{ onConfirmDeleteImageClick }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
tags="{ props.tags }"
|
||||
to-delete="{ state.toDelete }"
|
||||
></confirm-delete-image>
|
||||
<material-card class="taglist">
|
||||
<table style="border: none;">
|
||||
<table style="border: none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="creation-date { (state.desc && state.orderType === 'date') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
|
||||
onclick="{() => onPageReorder('date') }">
|
||||
onclick="{() => onPageReorder('date') }"
|
||||
>
|
||||
Creation date
|
||||
</th>
|
||||
<th
|
||||
class="image-size { (state.desc && state.orderType === 'size') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
|
||||
onclick="{() => onPageReorder('size') }">
|
||||
onclick="{() => onPageReorder('size') }"
|
||||
>
|
||||
Size
|
||||
</th>
|
||||
<th id="image-content-digest-header" if="{ props.showContentDigest }">Content Digest</th>
|
||||
|
||||
<th id="image-tag-header"
|
||||
<th
|
||||
id="image-tag-header"
|
||||
class="{ props.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
|
||||
onclick="{ onReverseOrder }">Tag
|
||||
onclick="{ onReverseOrder }"
|
||||
>
|
||||
Tag
|
||||
</th>
|
||||
<th class="show-tag-history">History</th>
|
||||
<th class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
|
||||
if="{ props.isImageRemoveActivated }">
|
||||
<material-checkbox class="indeterminate" checked="{ state.multiDelete }"
|
||||
<th
|
||||
class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
|
||||
if="{ props.isImageRemoveActivated }"
|
||||
>
|
||||
<material-checkbox
|
||||
class="indeterminate"
|
||||
checked="{ state.multiDelete }"
|
||||
if="{ state.toDelete.size === 0 || state.singleDeleteAction }"
|
||||
title="Toggle multi-delete. Alt+Click to select all tags." onChange="{ onRemoveImageHeaderChange }">
|
||||
title="Toggle multi-delete. Alt+Click to select all tags."
|
||||
onChange="{ onRemoveImageHeaderChange }"
|
||||
>
|
||||
</material-checkbox>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd"
|
||||
title="This will delete selected images." onClick="{ deleteImages }"
|
||||
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }">
|
||||
<material-button
|
||||
waves-center="true"
|
||||
rounded="true"
|
||||
waves-color="#ddd"
|
||||
title="This will delete selected images."
|
||||
onClick="{ deleteImages }"
|
||||
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }"
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
</th>
|
||||
@@ -63,30 +85,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</td>
|
||||
<td if="{ props.showContentDigest }">
|
||||
<image-content-digest image="{ image }" />
|
||||
<copy-to-clipboard target="digest" image="{ image }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" />
|
||||
<copy-to-clipboard
|
||||
target="digest"
|
||||
image="{ image }"
|
||||
pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<image-tag image="{ image }" />
|
||||
<copy-to-clipboard target="tag" image="{ image }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" />
|
||||
<copy-to-clipboard
|
||||
target="tag"
|
||||
image="{ image }"
|
||||
pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }"
|
||||
/>
|
||||
</td>
|
||||
<td class="show-tag-history">
|
||||
<tag-history-button image="{ image }" />
|
||||
</td>
|
||||
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
|
||||
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
|
||||
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
|
||||
on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }" />
|
||||
<remove-image
|
||||
multi-delete="{ state.multiDelete }"
|
||||
image="{ image }"
|
||||
registry-url="{ props.registryUrl }"
|
||||
handleCheckboxChange="{ onRemoveImageChange }"
|
||||
checked="{ state.toDelete.has(image) }"
|
||||
on-notify="{ props.onNotify }"
|
||||
on-authentication="{ props.onAuthentication }"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</material-card>
|
||||
<script>
|
||||
import {
|
||||
getPage,
|
||||
} from '../../scripts/utils';
|
||||
import { getPage } from '../../scripts/utils';
|
||||
import ImageDate from './image-date.riot';
|
||||
import ImageSize from './image-size.riot';
|
||||
import ImageTag from './image-tag.riot';
|
||||
@@ -94,9 +128,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import CopyToClipboard from './copy-to-clipboard.riot';
|
||||
import TagHistoryButton from './tag-history-button.riot';
|
||||
import RemoveImage from './remove-image.riot';
|
||||
import {
|
||||
matchSearch
|
||||
} from '../search-bar.riot';
|
||||
import { matchSearch } from '../search-bar.riot';
|
||||
import ConfirmDeleteImage from '../dialogs/confirm-delete-image.riot';
|
||||
const ACTION_CHECK_TO_DELETE = 'CHECK';
|
||||
const ACTION_UNCHECK_TO_DELETE = 'UNCHECK';
|
||||
@@ -118,18 +150,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
toDelete: new Set(),
|
||||
multiDelete: false,
|
||||
page: props.page,
|
||||
}
|
||||
};
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
if (state.page !== props.page) {
|
||||
state.toDelete.clear();
|
||||
}
|
||||
state.page = props.page
|
||||
state.page = props.page;
|
||||
},
|
||||
deleteImages() {
|
||||
this.update({
|
||||
confirmDeleteImage: true
|
||||
})
|
||||
confirmDeleteImage: true,
|
||||
});
|
||||
},
|
||||
onConfirmDeleteImageClick() {
|
||||
if (this.state.singleDeleteAction) {
|
||||
@@ -137,22 +169,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
this.update({
|
||||
singleDeleteAction: false,
|
||||
confirmDeleteImage: false
|
||||
})
|
||||
confirmDeleteImage: false,
|
||||
});
|
||||
},
|
||||
onRemoveImageHeaderChange(checked, event) {
|
||||
if (event.altKey === true) {
|
||||
const tags = getPage(this.props.tags, this.props.page);
|
||||
tags.filter(image => matchSearch(this.props.filterResults, image.tag))
|
||||
.forEach(tag => this.state.toDelete.add(tag));
|
||||
tags
|
||||
.filter((image) => matchSearch(this.props.filterResults, image.tag))
|
||||
.forEach((tag) => this.state.toDelete.add(tag));
|
||||
this.update({
|
||||
multiDelete: true,
|
||||
toDelete: this.state.toDelete
|
||||
})
|
||||
toDelete: this.state.toDelete,
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
multiDelete: checked
|
||||
})
|
||||
multiDelete: checked,
|
||||
});
|
||||
}
|
||||
},
|
||||
onRemoveImageChange(action, image) {
|
||||
@@ -177,8 +210,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
this.update({
|
||||
toDelete: this.state.toDelete,
|
||||
confirmDeleteImage,
|
||||
singleDeleteAction
|
||||
})
|
||||
singleDeleteAction,
|
||||
});
|
||||
},
|
||||
onReverseOrder() {
|
||||
this.state.orderType = null;
|
||||
@@ -188,30 +221,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
onPageReorder(type) {
|
||||
this.update({
|
||||
orderType: type,
|
||||
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc
|
||||
})
|
||||
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc,
|
||||
});
|
||||
},
|
||||
getPage(tags, page) {
|
||||
const sortedTags = getPage(tags, page);
|
||||
if (this.state.orderType === 'date') {
|
||||
sortedTags.sort((e1, e2) =>
|
||||
!this.state.desc ?
|
||||
e2.creationDate.getTime() - e1.creationDate.getTime() :
|
||||
e1.creationDate.getTime() - e2.creationDate.getTime());
|
||||
!this.state.desc
|
||||
? e2.creationDate.getTime() - e1.creationDate.getTime()
|
||||
: e1.creationDate.getTime() - e2.creationDate.getTime()
|
||||
);
|
||||
} else if (this.state.orderType === 'size') {
|
||||
sortedTags.sort((e1, e2) =>
|
||||
!this.state.desc ?
|
||||
e2.size - e1.size :
|
||||
e1.size - e2.size);
|
||||
sortedTags.sort((e1, e2) => (!this.state.desc ? e2.size - e1.size : e1.size - e2.size));
|
||||
}
|
||||
return sortedTags;
|
||||
},
|
||||
matchSearch
|
||||
}
|
||||
export {
|
||||
ACTION_CHECK_TO_DELETE,
|
||||
ACTION_UNCHECK_TO_DELETE,
|
||||
ACTION_DELETE_IMAGE
|
||||
}
|
||||
matchSearch,
|
||||
};
|
||||
export { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE };
|
||||
</script>
|
||||
</tag-table>
|
||||
</tag-table>
|
||||
|
||||
132
src/index.html
132
src/index.html
@@ -16,62 +16,78 @@
|
||||
-->
|
||||
<!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}"
|
||||
default-registries="${DEFAULT_REGISTRIES}" read-only-registries="${READ_ONLY_REGISTRIES}">
|
||||
</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}"
|
||||
>
|
||||
</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"
|
||||
>
|
||||
</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>
|
||||
|
||||
@@ -56,6 +56,7 @@ export class DockerImage {
|
||||
onNotify,
|
||||
onAuthentication,
|
||||
};
|
||||
this.ociImage = false;
|
||||
observable(this);
|
||||
this.on('get-size', function () {
|
||||
if (this.size !== undefined) {
|
||||
@@ -96,7 +97,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,11 +108,12 @@ 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 = self.ociImage ? response.manifests : response.layers;
|
||||
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) {
|
||||
@@ -121,8 +123,15 @@ export class DockerImage {
|
||||
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,7 +140,7 @@ 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' : '')
|
||||
);
|
||||
oReq.send();
|
||||
@@ -140,7 +149,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);
|
||||
self.creationDate = new Date(response.created);
|
||||
self.blobs = response;
|
||||
@@ -155,7 +164,7 @@ 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) {
|
||||
} else if (this.status === 404) {
|
||||
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found`, true);
|
||||
} else {
|
||||
self.opts.onNotify(this.responseText);
|
||||
|
||||
@@ -52,7 +52,7 @@ export class Http {
|
||||
switch (e) {
|
||||
case 'loadend': {
|
||||
self.oReq.addEventListener('loadend', function () {
|
||||
if (this.status == 401 && !this.withCredentials) {
|
||||
if (this.status === 401 && !this.withCredentials) {
|
||||
const tokenAuth =
|
||||
this.hasHeader('www-authenticate') && parseAuthenticateHeader(this.getResponseHeader('www-authenticate'));
|
||||
self.onAuthentication(tokenAuth, (bearer) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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)));
|
||||
@@ -76,7 +76,10 @@ export function getHistoryIcon(attribute) {
|
||||
case 'ExposedPorts':
|
||||
return 'router';
|
||||
default:
|
||||
'';
|
||||
if (attribute.startsWith('custom-label-')) {
|
||||
return 'label';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,3 +204,7 @@ export function decodeURI(url) {
|
||||
export function truthy(value) {
|
||||
return value === true || value === 'true';
|
||||
}
|
||||
|
||||
export function stringToArray(value) {
|
||||
return value && typeof value === 'string' ? value.split(',') : [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user