mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2026-02-19 21:29:51 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be813e6617 | ||
|
|
b4e6369a71 | ||
|
|
c9ede6fe61 | ||
|
|
656914f0d7 | ||
|
|
f8c5010fd1 | ||
|
|
42f19fcef7 | ||
|
|
f9620639bf | ||
|
|
d1700ccf74 | ||
|
|
991eaf932d | ||
|
|
e2ee319d4a | ||
|
|
06d6293e79 | ||
|
|
00fe443a7c | ||
|
|
6e7fc1508e | ||
|
|
178cd5a59d | ||
|
|
da9591609e | ||
|
|
f0a40d6087 | ||
|
|
01d8bcfccd | ||
|
|
c60c2f3e95 | ||
|
|
241ee0fd13 | ||
|
|
2e915a82b1 |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -27,7 +27,7 @@ My docker-compose file
|
||||
|
||||
```
|
||||
|
||||
My private docker registry configuration
|
||||
My private docker registry configuration
|
||||
```yml
|
||||
|
||||
```
|
||||
@@ -41,18 +41,18 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## System information
|
||||
|
||||
|
||||
- OS: [e.g. Debian 10, Windows, Android 9...]
|
||||
<!-- Browser is only for UI bugs -->
|
||||
- Browser:
|
||||
- Name: [e.g. Chrome, Firefox...]
|
||||
- Version: [e.g. 22]
|
||||
- Docker registry UI:
|
||||
- Version: [e.g. 1.3.0]
|
||||
- Interface variant: [standard or static]
|
||||
- Version: [e.g. 1.4.0]
|
||||
- Server: [docker or dist]
|
||||
<!-- Only for Docker and for where the UI is hosted -->
|
||||
- Docker version: [e.g. 19.03]
|
||||
- Docker registry ui tag: [latest, static, master, 1.4-static...]
|
||||
- OS/Arch: [e.g. linux/amd64]
|
||||
- Tools: [e.g. docker-compose, kubernets..]
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ node_modules
|
||||
package-lock.json
|
||||
registry-data
|
||||
.idea
|
||||
_site
|
||||
_site
|
||||
*.orig
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
- Sébastien Huss [@sebt3](https://github.com/sebt3)
|
||||
- Vladimir Kozyrev [@fieryvova](https://github.com/fieryvova)
|
||||
- Haibo Jia [@bluethon](https://github.com/bluethon)
|
||||
- Manuel Leitold [@agrippa1994](https://github.com/agrippa1994)
|
||||
- Murad [@muradheydarov](https://github.com/muradheydarov)
|
||||
|
||||
## Because committers are not the only contributors
|
||||
|
||||
@@ -26,4 +28,7 @@
|
||||
- [@marcusblake](https://github.com/marcusblake)
|
||||
- Dario [@pidario](https://github.com/pidario)
|
||||
- Jernej K. [@Cvetk0](https://github.com/Cvetk0)
|
||||
- Cristian Posoiu [@cr1st1p](https://github.com/cr1st1p)
|
||||
- Cristian Posoiu [@cr1st1p](https://github.com/cr1st1p)
|
||||
- Sepp Zuther [@Herr-Sepp](https://github.com/Herr-Sepp)
|
||||
- Tomas Hulata [@tombokombo](https://github.com/tombokombo)
|
||||
- Ben Jackson [@bjj](https://github.com/bjj)
|
||||
17
README.md
17
README.md
@@ -46,6 +46,9 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- Add Title when using `REGISTRY_URL` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)) **static interface**.
|
||||
- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)) **static interface**.
|
||||
- Add custom header via environment variable and file via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) **static interface**
|
||||
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)).
|
||||
- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)).
|
||||
- Multi arch support in history page (see [#130](https://github.com/Joxit/docker-registry-ui/issues/130) and [#134](https://github.com/Joxit/docker-registry-ui/pull/134))
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -65,6 +68,8 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- This fixes the issue [#88](https://github.com/Joxit/docker-registry-ui/issues/88). More about this in [#113](https://github.com/Joxit/docker-registry-ui/issues/113).
|
||||
- Why DELETE fails with 401 status code (using Basic Auth) ?
|
||||
- This is caused by a bug in docker registry, I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104)).
|
||||
- Can I use the docker registry ui as a standalone application (with Electron) ?
|
||||
- Yes, check out the example [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron). (see [#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
|
||||
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples) or open an issue.
|
||||
|
||||
@@ -181,9 +186,12 @@ http:
|
||||
headers:
|
||||
Access-Control-Allow-Origin: ['<your docker-registry-ui url>']
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS'] # Optional
|
||||
```
|
||||
|
||||
An alternative for CORS issues is a plugin on your browser, more info [here](https://github.com/Joxit/docker-registry-ui/issues/25#issuecomment-621104846) (thank you [xmontero](https://github.com/xmontero)).
|
||||
|
||||
## Using delete
|
||||
|
||||
For deleting images, you need to activate the delete feature in your registry:
|
||||
@@ -225,7 +233,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://127.0.0.1:8001']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
@@ -235,6 +243,10 @@ auth:
|
||||
path: /etc/docker/registry/htpasswd
|
||||
```
|
||||
|
||||
## Standalone Application
|
||||
If you do not want to install the docker-registry-ui on your server, you may
|
||||
check out the [Electron](examples/electron/README.md) standalone application.
|
||||
|
||||
## All examples
|
||||
|
||||
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
|
||||
@@ -245,4 +257,5 @@ auth:
|
||||
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
|
||||
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
|
||||
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
|
||||
@@ -8,6 +8,14 @@ if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
|
||||
sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
if [ "${SHOW_CONTENT_DIGEST}" = false ] ; then
|
||||
sed -i -r "s/(showContentDigest[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
if [ -n "${CATALOG_ELEMENTS_LIMIT}" ] ; then
|
||||
sed -i -r "s/(catalogElementsLimit[:=])[^,;]*/\1${CATALOG_ELEMENTS_LIMIT}/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
get_nginx_proxy_headers() {
|
||||
(
|
||||
env &&
|
||||
|
||||
2
dist/scripts/docker-registry-ui-static.js
vendored
2
dist/scripts/docker-registry-ui-static.js
vendored
File diff suppressed because one or more lines are too long
2
dist/scripts/docker-registry-ui.js
vendored
2
dist/scripts/docker-registry-ui.js
vendored
File diff suppressed because one or more lines are too long
2
dist/style.css
vendored
2
dist/style.css
vendored
File diff suppressed because one or more lines are too long
8
examples/electron/.gitignore
vendored
Normal file
8
examples/electron/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# NPM renames .gitignore to .npmignore
|
||||
# In order to prevent that, we remove the initial "."
|
||||
# And the CLI then renames it
|
||||
|
||||
dist/
|
||||
node_modules/
|
||||
Registry*
|
||||
.cache
|
||||
57
examples/electron/README.md
Normal file
57
examples/electron/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Standalone Application
|
||||
|
||||
## Overview
|
||||
|
||||
This standalone application is based on Electron which encapsulates the whole
|
||||
docker-registry-ui in a single executable, that can be run on your local
|
||||
computer.
|
||||
|
||||
## Building
|
||||
* Check out or download the repository, open a terminal at the checkout
|
||||
directory, download the dependencies and build the web app:
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
* After building the web application, navigate to the ```electron``` directory
|
||||
and execute following commands to build the executable:
|
||||
```bash
|
||||
cd electron
|
||||
npm install
|
||||
npm run dist
|
||||
```
|
||||
If you encounter any issues, please check the troubleshooting below.
|
||||
|
||||
|
||||
## Password Protected Registries
|
||||
If you want to interact with password protected Docker Registries, this
|
||||
application will use the keystore of your system to gather the credentials for
|
||||
accessing the Registry.
|
||||
|
||||
This is accomplished with the [keytar](https://www.npmjs.com/package/keytar)
|
||||
package. In concjunction with keytar, the integrated credential
|
||||
helper supports you with managing the credentials to the Registries.
|
||||
|
||||

|
||||
|
||||
|
||||
## Troubleshooting
|
||||
* Problem: The application does not start with ```npm start``` and exits with following message:
|
||||
```
|
||||
[7742:0509/001117.199224:FATAL:setuid_sandbox_host.cc(157)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that ./node_modules/electron dist/chrome-sandbox is owned by root and has mode 4755.
|
||||
```
|
||||
|
||||
Solution: Add proper rights to the chrome-sanbox
|
||||
```bash
|
||||
sudo chown root ./node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
|
||||
```
|
||||
|
||||
* Problem: I am on Linux and to not have any password wallet for keytar.
|
||||
|
||||
Solution: Install following dependencies according to the official [setup instructions](https://atom.github.io/node-keytar/) for keytar on Linux:
|
||||
* Debian/Ubuntu: ```sudo apt-get install libsecret-1-dev```
|
||||
* Red Hat-based: ```sudo yum install libsecret-devel```
|
||||
* Arch Linux: ```sudo pacman -S libsecret```
|
||||
|
||||
|
||||
8
examples/electron/authentication/index.html
Normal file
8
examples/electron/authentication/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
211
examples/electron/authentication/index.tsx
Normal file
211
examples/electron/authentication/index.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import * as React from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {render} from "react-dom";
|
||||
import * as keytar from 'keytar';
|
||||
import {ipcRenderer} from 'electron';
|
||||
import {
|
||||
Button,
|
||||
createMuiTheme,
|
||||
CssBaseline,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
ThemeProvider,
|
||||
useMediaQuery
|
||||
} from "@material-ui/core";
|
||||
import {Alert, AlertTitle} from '@material-ui/lab';
|
||||
import {blue} from "@material-ui/core/colors";
|
||||
import {Add as AddIcon, Delete as DeleteIcon, Save as SaveIcon} from "@material-ui/icons";
|
||||
|
||||
const mainStyle = makeStyles((theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
main: {
|
||||
flexGrow: 1,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
function CredentialRow({credential, index, onDelete, onUpdate}) {
|
||||
const [account, setAccount] = useState(credential?.account || '');
|
||||
const [password, setPassword] = useState(credential?.password || '');
|
||||
|
||||
const style = mainStyle();
|
||||
return (<TableRow>
|
||||
<TableCell>
|
||||
<TextField
|
||||
className={style.input}
|
||||
type="text"
|
||||
placeholder='https://user@someregistry:5000/'
|
||||
value={account} variant="outlined"
|
||||
onChange={(e) => {
|
||||
setAccount(e.target.value)
|
||||
}}/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField type="password"
|
||||
className={style.input}
|
||||
variant="outlined"
|
||||
placeholder='Password'
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value)
|
||||
}}/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={async () => await onUpdate(credential, index, {account, password})}>
|
||||
<SaveIcon/>
|
||||
</IconButton>
|
||||
<IconButton onClick={async () => await onDelete(credential, index,)}>
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>);
|
||||
}
|
||||
|
||||
|
||||
function CredentialsTable({onError}) {
|
||||
const [credentials, setCredentials] = useState(null);
|
||||
|
||||
async function loadItems() {
|
||||
try {
|
||||
const credentials = await keytar.findCredentials('docker-registry-ui');
|
||||
for (const credential of credentials) {
|
||||
// fix for windows
|
||||
credential.password = credential.password.replace(/\000+/g, '');
|
||||
}
|
||||
setCredentials(credentials);
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item, index) {
|
||||
// delete an item that has not been stored yet
|
||||
if (!item) {
|
||||
const newCredentials = [...credentials];
|
||||
newCredentials.splice(index, 1);
|
||||
setCredentials(newCredentials);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await keytar.deletePassword('docker-registry-ui', item.account);
|
||||
await loadItems();
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate(oldCredentials, index, newCredentials) {
|
||||
try {
|
||||
await handleDelete(oldCredentials, index);
|
||||
await keytar.setPassword('docker-registry-ui', newCredentials.account, newCredentials.password);
|
||||
await loadItems();
|
||||
} catch (e) {
|
||||
console.error("Error while updating key: ", e);
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
await loadItems();
|
||||
};
|
||||
|
||||
load();
|
||||
return;
|
||||
}, []);
|
||||
|
||||
if (credentials === null) {
|
||||
return <LinearProgress/>
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Host of the registry including username</TableCell>
|
||||
<TableCell>Password</TableCell>
|
||||
<TableCell align='right'>
|
||||
<IconButton onClick={() => {
|
||||
setCredentials([...credentials, null])
|
||||
}} disabled={credentials.includes(null)}>
|
||||
<AddIcon/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{credentials.map((credential, index) => <CredentialRow
|
||||
onDelete={handleDelete}
|
||||
onUpdate={handleUpdate}
|
||||
index={index}
|
||||
key={credential?.account || ''}
|
||||
credential={credential}/>)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
const [error, setError] = useState();
|
||||
const classes = mainStyle();
|
||||
|
||||
const theme = React.useMemo(
|
||||
() =>
|
||||
createMuiTheme({
|
||||
palette: {
|
||||
type: prefersDarkMode ? 'dark' : 'light',
|
||||
primary: blue,
|
||||
},
|
||||
}),
|
||||
[prefersDarkMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline/>
|
||||
<div className={classes.root}>
|
||||
{error && <Alert severity='error' onClose={() => {
|
||||
setError(null)
|
||||
}}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
{error}
|
||||
</Alert>}
|
||||
<main className={classes.main}>
|
||||
<CredentialsTable onError={setError}/>
|
||||
</main>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(<App/>, document.getElementById("root"));
|
||||
|
||||
// @ts-ignore
|
||||
if (module.hot) {
|
||||
// @ts-ignore
|
||||
module.hot.accept();
|
||||
}
|
||||
|
||||
BIN
examples/electron/doc/assets/authentication.gif
Normal file
BIN
examples/electron/doc/assets/authentication.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
229
examples/electron/index.js
Normal file
229
examples/electron/index.js
Normal file
@@ -0,0 +1,229 @@
|
||||
const {app, BrowserWindow, globalShortcut, Menu} = require('electron');
|
||||
const isDevMode = require('electron-is-dev');
|
||||
const keytar = require('keytar');
|
||||
const url = require('url');
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
// Place holders for our windows so they don't get garbage collected.
|
||||
let mainWindow = null;
|
||||
|
||||
// Credentials that are fetched from the Keychain
|
||||
let credentials = [];
|
||||
|
||||
// Credentials helper window
|
||||
let credentialsWindow;
|
||||
|
||||
const template = [
|
||||
// { role: 'appMenu' }
|
||||
...(isMac ? [{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{role: 'about'},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Preferences', accelerator: 'CmdorCtrl+,', click: () => {
|
||||
credentialsWindow.show();
|
||||
}
|
||||
},
|
||||
{type: 'separator'},
|
||||
{role: 'hide'},
|
||||
{role: 'hideothers'},
|
||||
{role: 'unhide'},
|
||||
{type: 'separator'},
|
||||
{role: 'quit'}
|
||||
]
|
||||
}] : []),
|
||||
// { role: 'fileMenu' }
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
...(isMac ? [] : [{role: 'quit'}]),
|
||||
{
|
||||
label: 'Preferences', accelerator: 'CmdorCtrl+,', click: () => {
|
||||
credentialsWindow.show();
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
// { role: 'editMenu' }
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{role: 'undo'},
|
||||
{role: 'redo'},
|
||||
{type: 'separator'},
|
||||
{role: 'cut'},
|
||||
{role: 'copy'},
|
||||
{role: 'paste'},
|
||||
...(isMac ? [
|
||||
{role: 'pasteAndMatchStyle'},
|
||||
{role: 'delete'},
|
||||
{role: 'selectAll'},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Speech',
|
||||
submenu: [
|
||||
{role: 'startspeaking'},
|
||||
{role: 'stopspeaking'}
|
||||
]
|
||||
}
|
||||
] : [
|
||||
{role: 'delete'},
|
||||
{type: 'separator'},
|
||||
{role: 'selectAll'}
|
||||
])
|
||||
]
|
||||
},
|
||||
// { role: 'viewMenu' }
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{role: 'reload'},
|
||||
{role: 'forcereload'},
|
||||
{role: 'toggledevtools'},
|
||||
{type: 'separator'},
|
||||
{role: 'resetzoom'},
|
||||
{role: 'zoomin'},
|
||||
{role: 'zoomout'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen'},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Credentials Helper', accelerator: 'CmdorCtrl+k', click: () => {
|
||||
credentialsWindow.show();
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
// { role: 'windowMenu' }
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{role: 'minimize'},
|
||||
{role: 'zoom'},
|
||||
...(isMac ? [
|
||||
{type: 'separator'},
|
||||
{role: 'front'},
|
||||
{type: 'separator'},
|
||||
{role: 'window'}
|
||||
] : [
|
||||
{role: 'close'}
|
||||
])
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
click: async () => {
|
||||
const {shell} = require('electron')
|
||||
await shell.openExternal('https://joxit.dev/docker-registry-ui/')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
if (isMac) {
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
async function loadCredentials() {
|
||||
try {
|
||||
credentials = await keytar.findCredentials('docker-registry-ui');
|
||||
for (const credential of credentials) {
|
||||
// fix for windows
|
||||
credential.password = credential.password.replace(/\000+/g, '');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
credentials = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function createWindow() {
|
||||
return new Promise((resolve, reject) => {
|
||||
mainWindow = new BrowserWindow({
|
||||
height: 920,
|
||||
width: 1600,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
}
|
||||
});
|
||||
|
||||
if (isDevMode) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
mainWindow.setMenu(menu);
|
||||
}
|
||||
|
||||
mainWindow.loadURL(`file://${__dirname}/dist/index.html`);
|
||||
mainWindow.webContents.on('dom-ready', () => {
|
||||
console.log("Main Window DOM ready");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createCredentialsWindow() {
|
||||
return new Promise((resolve) => {
|
||||
credentialsWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 400,
|
||||
show: false,
|
||||
title: 'Credential Manager',
|
||||
parent: mainWindow,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (isDevMode) {
|
||||
credentialsWindow.openDevTools();
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
credentialsWindow.setMenu(null);
|
||||
}
|
||||
|
||||
credentialsWindow.loadURL(`file://${__dirname}/dist/authentication/index.html`);
|
||||
credentialsWindow.webContents.on('dom-ready', () => {
|
||||
console.log('Credentials Window DOM is ready');
|
||||
resolve();
|
||||
});
|
||||
|
||||
credentialsWindow.on('close', async (e) => {
|
||||
console.log("Closed credential window");
|
||||
credentialsWindow.hide();
|
||||
e.preventDefault();
|
||||
await loadCredentials();
|
||||
mainWindow.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.on('ready', async () => {
|
||||
await Promise.all([
|
||||
loadCredentials(),
|
||||
createWindow(),
|
||||
createCredentialsWindow(),
|
||||
]);
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
|
||||
app.on("login", (event, contents, authencation, info, callback) => {
|
||||
for (const credential of credentials) {
|
||||
const parsedUrl = url.parse(credential.account);
|
||||
if (parsedUrl.hostname === info.host) {
|
||||
return callback(parsedUrl.auth, credential.password);
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
39
examples/electron/package.json
Normal file
39
examples/electron/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "docker-registry-ui",
|
||||
"version": "1.4.8",
|
||||
"productName": "Registry UI",
|
||||
"description": "Electron Application for Docker Registry UI",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "electron ./",
|
||||
"start:dev": "parcel serve -d dist/authentication -t electron --public-url ./ authentication/index.html",
|
||||
"build": "parcel build -d dist/authentication -t electron --public-url ./ authentication/index.html",
|
||||
"rebuild": "electron-rebuild -f -w keytar",
|
||||
"package": "electron-packager --overwrite .",
|
||||
"sync": "copyfiles ../../dist/* ../../dist/**/* ./examples/out",
|
||||
"dist": "npm run rebuild && npm run sync && npm run build && npm run package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.52",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"keytar": "^5.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"electron": "^8.0.0",
|
||||
"electron-builder": "^22.6.0",
|
||||
"electron-packager": "^14.2.1",
|
||||
"electron-rebuild": "^1.10.1",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"keywords": [
|
||||
"electron"
|
||||
],
|
||||
"author": "",
|
||||
"license": "AGPL-3.0"
|
||||
}
|
||||
@@ -63,7 +63,7 @@ registry:
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: registry
|
||||
tag: 2.6.2
|
||||
tag: 2.7.1
|
||||
pullPolicy: Always
|
||||
probe:
|
||||
liveness: true
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '2'
|
||||
|
||||
services:
|
||||
registry-srv:
|
||||
image: registry:2.7.1
|
||||
image: registry:2.7
|
||||
restart: always
|
||||
volumes:
|
||||
- storage:/var/lib/registry
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
networks:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
networks:
|
||||
|
||||
@@ -28,7 +28,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://127.0.0.1']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -15,6 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['*']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Accept']
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
health:
|
||||
storagedriver:
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '2'
|
||||
|
||||
services:
|
||||
registry-srv:
|
||||
image: registry:latest
|
||||
image: registry:2.7
|
||||
restart: always
|
||||
volumes:
|
||||
- storage:/var/lib/registry
|
||||
|
||||
@@ -25,7 +25,7 @@ data:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['*']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
claimName: docker-registry
|
||||
containers:
|
||||
- name: registry
|
||||
image: "docker.io/registry:2.6.2"
|
||||
image: "docker.io/registry:2.7"
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: registry
|
||||
|
||||
@@ -15,7 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.1'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- /opt/docker-registry:/var/lib/registry
|
||||
environment:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
- ./registry-config/credentials.yml:/etc/docker/registry/config.yml
|
||||
|
||||
@@ -15,7 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./registry-data:/var/lib/registry
|
||||
networks:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
ports:
|
||||
- 5000:5000
|
||||
volumes:
|
||||
|
||||
@@ -15,7 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
|
||||
@@ -15,7 +15,7 @@ http:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://localhost']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
registry:
|
||||
image: registry:2.6.2
|
||||
image: registry:2.7
|
||||
ports:
|
||||
- 5000:5000
|
||||
volumes:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "docker-registry-ui",
|
||||
"version": "1.4.4",
|
||||
"version": "1.5.0",
|
||||
"scripts": {
|
||||
"build": "./node_modules/gulp/bin/gulp.js build"
|
||||
"build": "./node_modules/gulp/bin/gulp.js build",
|
||||
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
var registryUI = {}
|
||||
registryUI.URL_QUERY_PARAM_REGEX = /[&?]url=/;
|
||||
registryUI.URL_PARAM_REGEX = /^url=/;
|
||||
registryUI.showContentDigest = true;
|
||||
registryUI.catalogElementsLimit = 100000;
|
||||
|
||||
registryUI.url = function(byPassQueryParam) {
|
||||
if (!registryUI._url) {
|
||||
|
||||
@@ -33,6 +33,8 @@ registryUI.name = function() {
|
||||
};
|
||||
registryUI.pullUrl = '${PULL_URL}';
|
||||
registryUI.isImageRemoveActivated = true;
|
||||
registryUI.showContentDigest = true;
|
||||
registryUI.catalogElementsLimit = 100000;
|
||||
registryUI.catalog = {};
|
||||
registryUI.taglist = {};
|
||||
registryUI.taghistory = {};
|
||||
|
||||
@@ -119,3 +119,9 @@ registryUI.stripHttps = function (url) {
|
||||
}
|
||||
return url.replace(/^https?:\/\//, '');
|
||||
};
|
||||
|
||||
registryUI.eventTransfer = function(from, to) {
|
||||
from.on('*', function(event, param) {
|
||||
to.trigger(event, param);
|
||||
})
|
||||
}
|
||||
@@ -48,7 +48,7 @@ main {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
material-card, pagination .conatianer {
|
||||
material-card, material-tabs, pagination .conatianer {
|
||||
max-width: 95%;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
@@ -68,11 +68,34 @@ pagination .conatianer .pagination-centered {
|
||||
|
||||
/* 1515px * 0.95 = 1440px */
|
||||
@media screen and (min-width: 1515px){
|
||||
material-card, pagination .conatianer {
|
||||
material-card, material-tabs, pagination .conatianer {
|
||||
max-width: 1440px;
|
||||
}
|
||||
}
|
||||
|
||||
material-tabs {
|
||||
display: block;
|
||||
-webkit-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16), 0 2px 10px 0 rgba(0,0,0,.12);
|
||||
-ms-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
-moz-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
-o-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16), 0 2px 10px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
material-tabs material-button,
|
||||
material-tabs material-button .content .text {
|
||||
background-color: #fff;
|
||||
color: #aaa;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
material-tabs .line-wrapper .line {
|
||||
background-color: #25313b;
|
||||
}
|
||||
material-tabs material-button.selected .content .text {
|
||||
color: #25313b;
|
||||
}
|
||||
|
||||
material-spinner {
|
||||
align-self: center;
|
||||
}
|
||||
@@ -282,6 +305,11 @@ material-button .content i.material-icons,
|
||||
color: #777;
|
||||
}
|
||||
|
||||
material-button[disabled] .content i.material-icons,
|
||||
material-checkbox[disabled] .content i.material-icons {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
material-snackbar .toast {
|
||||
height: auto;
|
||||
}
|
||||
@@ -371,6 +399,10 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
material-footer {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 760px) and (max-height: 750px) {
|
||||
main {
|
||||
min-height: calc(100% - 144px - 2.5em);
|
||||
@@ -437,6 +469,14 @@ image-content-digest {
|
||||
padding: 7px 5px;
|
||||
}
|
||||
|
||||
taglist .creation-date {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
taglist .image-size {
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
catalog material-card,
|
||||
tag-history material-card {
|
||||
min-height: auto;
|
||||
@@ -457,6 +497,7 @@ tag-history-element {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
min-width: 100px;
|
||||
min-height: 3em;
|
||||
width: 420px;
|
||||
float: left;
|
||||
overflow-x: auto;
|
||||
@@ -511,8 +552,14 @@ catalog-element catalog-element.hide material-card {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
catalog-element catalog-element .list > span i.material-icons {
|
||||
margin-right: 48px;
|
||||
catalog-element catalog-element > .content {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1515px){
|
||||
catalog-element catalog-element > .content material-card {
|
||||
max-width: calc(1440px - 3em);
|
||||
}
|
||||
}
|
||||
|
||||
remove-image {
|
||||
|
||||
@@ -118,9 +118,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return char >= '0' && char <= '9';
|
||||
};
|
||||
|
||||
registryUI.DockerImage = function(name, tag) {
|
||||
registryUI.DockerImage = function(name, tag, list) {
|
||||
this.name = name;
|
||||
this.tag = tag;
|
||||
this.list = list;
|
||||
this.chars = 0;
|
||||
riot.observable(this);
|
||||
this.on('get-size', function() {
|
||||
@@ -192,6 +193,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
oReq.addEventListener('loadend', function() {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
const response = JSON.parse(this.responseText);
|
||||
if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') {
|
||||
self.trigger('list', response);
|
||||
const manifest = response.manifests[0];
|
||||
const image = new registryUI.DockerImage(self.name, manifest.digest)
|
||||
registryUI.eventTransfer(image, self)
|
||||
image.fillInfo()
|
||||
self.variants = [image];
|
||||
return;
|
||||
}
|
||||
self.size = response.layers.reduce(function(acc, e) {
|
||||
return acc + e.size;
|
||||
}, 0);
|
||||
@@ -202,6 +212,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
oReq.getContentDigest(function (digest) {
|
||||
self.digest = digest;
|
||||
self.trigger('content-digest', digest);
|
||||
if (!digest) {
|
||||
registryUI.showErrorCanNotReadContentDigest();
|
||||
}
|
||||
});
|
||||
self.getBlobs(response.config.digest)
|
||||
} else if (this.status == 404) {
|
||||
@@ -211,7 +224,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
});
|
||||
oReq.open('GET', registryUI.url() + '/v2/' + self.name + '/manifests/' + self.tag);
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json'
|
||||
+ (self.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : ''));
|
||||
oReq.send();
|
||||
};
|
||||
|
||||
|
||||
@@ -16,18 +16,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<catalog-element>
|
||||
<!-- Begin of tag -->
|
||||
<material-card class="list highlight" item="{item}" expanded="{expanded}">
|
||||
<material-waves onmousedown="{launch}" center="true" color="#ddd" />
|
||||
<span>
|
||||
<i class="material-icons">send</i>
|
||||
{ typeof opts.item === "string" ? opts.item : opts.item.repo }
|
||||
<div if="{typeof opts.item !== "string"}" class="item-count right">
|
||||
{ opts.item.images && opts.item.images.length } images
|
||||
<i class="material-icons animated {expanded: opts.expanded}">expand_more</i>
|
||||
</div>
|
||||
</span>
|
||||
</material-card>
|
||||
<catalog-element if="{typeof opts.item !== "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
|
||||
<div class="content">
|
||||
<material-card class="list highlight" item="{item}" expanded="{expanded}">
|
||||
<material-waves onmousedown="{launch}" center="true" color="#ddd" />
|
||||
<span>
|
||||
<i class="material-icons">send</i>
|
||||
{ typeof opts.item === "string" ? opts.item : opts.item.repo }
|
||||
<div if="{typeof opts.item !== "string"}" class="item-count right">
|
||||
{ opts.item.images && opts.item.images.length } images
|
||||
<i class="material-icons animated {expanded: opts.expanded}">expand_more</i>
|
||||
</div>
|
||||
</span>
|
||||
</material-card>
|
||||
<catalog-element if="{typeof opts.item !== "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
|
||||
</div>
|
||||
<script>
|
||||
this.on('mount', function() {
|
||||
const self = this;
|
||||
|
||||
@@ -68,7 +68,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
registryUI.catalog.loadend = true;
|
||||
registryUI.catalog.instance.update();
|
||||
});
|
||||
oReq.open('GET', registryUI.url() + '/v2/_catalog?n=100000');
|
||||
oReq.open('GET', registryUI.url() + '/v2/_catalog?n=' + registryUI.catalogElementsLimit);
|
||||
oReq.send();
|
||||
};
|
||||
registryUI.catalog.display();
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
if (chars >= 70) {
|
||||
self.display_id = self.digest;
|
||||
self.title = '';
|
||||
} else if (chars === 0) {
|
||||
} else if (chars <= 0) {
|
||||
self.display_id = '';
|
||||
self.title = self.digest;
|
||||
} else {
|
||||
|
||||
@@ -15,10 +15,10 @@ 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="{ !opts.multiDelete }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." if="{ !opts.multiDelete }" disabled="{ !this.digest }">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<material-checkbox if="{ opts.multiDelete }" title="Select this tag to delete it."></material-checkbox>
|
||||
<material-checkbox if="{ opts.multiDelete }" title="Select this tag to delete it." disabled="{ !this.digest }"></material-checkbox>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
|
||||
@@ -35,7 +35,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
const tag = self.opts.image.tag;
|
||||
registryUI.taglist.go(name);
|
||||
if (!self.digest) {
|
||||
registryUI.showErrorCanNotReadContentDigest();
|
||||
registryUI.snackbar('Information for ' + name + ':' + tag + ' are not yet loaded.');
|
||||
return;
|
||||
}
|
||||
const oReq = new Http();
|
||||
@@ -66,7 +66,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
self.tags['material-checkbox'].toggle();
|
||||
}
|
||||
self.tags['material-checkbox'].on('toggle', function() {
|
||||
registryUI.taglist.instance.trigger('toggle-remove-image', self.checked);
|
||||
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
|
||||
});
|
||||
}
|
||||
self.multiDelete = self.opts.multiDelete;
|
||||
@@ -74,6 +74,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
opts.image.one('content-digest', function(digest) {
|
||||
self.digest = digest;
|
||||
self.update();
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
this.on('mount', function() {
|
||||
const self = this;
|
||||
this.refs.button.root.onclick = function() {
|
||||
registryUI.taghistory._image = self.opts.image;
|
||||
registryUI.taghistory.go(self.opts.image.name, self.opts.image.tag);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -26,46 +26,48 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
</material-card>
|
||||
<div hide="{ registryUI.taghistory.loadend }" class="spinner-wrapper">
|
||||
<material-spinner/>
|
||||
<material-spinner />
|
||||
</div>
|
||||
|
||||
<material-tabs if="{ this.archs }" useLine="true" tabs="{ this.archs }" tabchanged="{ this.tabchanged }" />
|
||||
|
||||
<material-card each="{ guiElement in this.elements }" class="tag-history-element">
|
||||
<tag-history-element each="{ entry in guiElement }" if="{ entry.value && entry.value.length > 0}"/>
|
||||
<tag-history-element each="{ entry in guiElement }" if="{ entry.value && entry.value.length > 0}" />
|
||||
</material-card>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
const eltIdx = function(e) {
|
||||
const eltIdx = function (e) {
|
||||
switch (e) {
|
||||
case 'id': return 1;
|
||||
case 'created': return 2;
|
||||
case 'created_by': return 3;
|
||||
case 'size': return 4;
|
||||
case 'os': return 5;
|
||||
case 'architecture': return 6;
|
||||
case 'created': return 1;
|
||||
case 'created_by': return 2;
|
||||
case 'size': return 3;
|
||||
case 'os': return 4;
|
||||
case 'architecture': return 5;
|
||||
case 'id': return 6;
|
||||
case 'linux': return 7;
|
||||
case 'docker_version': return 8;
|
||||
default: return 10;
|
||||
}
|
||||
};
|
||||
|
||||
const eltSort = function(e1, e2) {
|
||||
const eltSort = function (e1, e2) {
|
||||
return eltIdx(e1.key) - eltIdx(e2.key);
|
||||
};
|
||||
|
||||
const modifySpecificAttributeTypes = function(attribute, value) {
|
||||
const modifySpecificAttributeTypes = function (attribute, value) {
|
||||
switch (attribute) {
|
||||
case 'created':
|
||||
return new Date(value).toLocaleString();
|
||||
case 'created_by':
|
||||
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
|
||||
return (cmd && cmd [1]) || 'RUN'
|
||||
return (cmd && cmd[1]) || 'RUN'
|
||||
case 'size':
|
||||
return registryUI.bytesToSize(value);
|
||||
case 'Entrypoint':
|
||||
case 'Cmd':
|
||||
return (value || []).join(' ');
|
||||
case 'Labels':
|
||||
return Object.keys(value || {}).map(function(elt) {
|
||||
return Object.keys(value || {}).map(function (elt) {
|
||||
return value[elt] ? elt + '=' + value[elt] : '';
|
||||
});
|
||||
case 'Volumes':
|
||||
@@ -75,14 +77,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return value || '';
|
||||
};
|
||||
|
||||
const getConfig = function(blobs) {
|
||||
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels', 'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts'].reduce(function(acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const getConfig = function (blobs) {
|
||||
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels', 'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts']
|
||||
.reduce(function (acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value && e === 'architecture' && blobs.variant) {
|
||||
acc[e] = value + blobs.variant;
|
||||
} else if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!res.author && (res.Labels && res.Labels.maintainer)) {
|
||||
res.author = blobs.config.Labels.maintainer;
|
||||
@@ -92,7 +97,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return res;
|
||||
};
|
||||
|
||||
const processBlobs = function(blobs) {
|
||||
const processBlobs = function (blobs) {
|
||||
function exec(elt) {
|
||||
const guiElements = [];
|
||||
for (var attribute in elt) {
|
||||
@@ -107,27 +112,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
return guiElements.sort(eltSort);
|
||||
}
|
||||
|
||||
self.elements.push(exec(getConfig(blobs)));
|
||||
blobs.history.reverse().forEach(function(elt) { self.elements.push(exec(elt)) });
|
||||
self.elements = new Array(blobs.history.length + 1);
|
||||
self.elements[0] = exec(getConfig(blobs));
|
||||
blobs.history.forEach(function (elt, i) { self.elements[blobs.history.length - i] = exec(elt) });
|
||||
registryUI.taghistory.loadend = true;
|
||||
self.update();
|
||||
};
|
||||
|
||||
registryUI.taghistory.display = function() {
|
||||
self.elements = []
|
||||
const blobs = registryUI.taghistory._image && registryUI.taghistory._image.blobs;
|
||||
if (blobs) {
|
||||
window.scrollTo(0, 0);
|
||||
return processBlobs(blobs);
|
||||
}
|
||||
const image = new registryUI.DockerImage(registryUI.taghistory.image, registryUI.taghistory.tag);
|
||||
image.fillInfo()
|
||||
image.on('blobs', processBlobs);
|
||||
const multiArchList = function (manifests) {
|
||||
manifests = manifests.manifests || manifests;
|
||||
self.archs = manifests.map(function (manifest) {
|
||||
return {
|
||||
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ? manifest.platform.variant : ''),
|
||||
digest: manifest.digest
|
||||
}
|
||||
})
|
||||
self.update();
|
||||
};
|
||||
|
||||
this.on('mount', function() {
|
||||
self.refs['tag-history-tag'].tags['material-button'].root.onclick = function() {
|
||||
self.tabchanged = function (arch, idx) {
|
||||
self.elements = []
|
||||
self.image.variants[idx] = self.image.variants[idx] || new registryUI.DockerImage(registryUI.taghistory.image, arch.digest);
|
||||
if (self.image.variants[idx].blobs) {
|
||||
return processBlobs(self.image.variants[idx].blobs);
|
||||
}
|
||||
self.image.variants[idx].fillInfo();
|
||||
self.image.variants[idx].on('blobs', processBlobs);
|
||||
};
|
||||
|
||||
registryUI.taghistory.display = function () {
|
||||
self.elements = []
|
||||
self.image = new registryUI.DockerImage(registryUI.taghistory.image, registryUI.taghistory.tag, true);
|
||||
self.image.fillInfo()
|
||||
self.image.on('blobs', processBlobs);
|
||||
self.image.on('list', multiArchList)
|
||||
};
|
||||
|
||||
this.on('mount', function () {
|
||||
self.refs['tag-history-tag'].tags['material-button'].root.onclick = function () {
|
||||
registryUI.taglist.go(registryUI.taghistory.image);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -41,9 +41,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<table show="{ registryUI.taglist.loadend }" style="border: none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Creation date</th>
|
||||
<th>Size</th>
|
||||
<th id="image-content-digest-header">Content Digest</th>
|
||||
<th class="creation-date">Creation date</th>
|
||||
<th class="image-size">Size</th>
|
||||
<th id="image-content-digest-header" if="{ registryUI.showContentDigest }">Content Digest</th>
|
||||
|
||||
<th
|
||||
id="image-tag-header"
|
||||
@@ -60,13 +60,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr each="{ image in this.opts.tags }">
|
||||
<td>
|
||||
<td class="creation-date">
|
||||
<image-date image="{ image }"/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="image-size">
|
||||
<image-size image="{ image }"/>
|
||||
</td>
|
||||
<td>
|
||||
<td if="{ registryUI.showContentDigest }">
|
||||
<image-content-digest image="{ image }"/>
|
||||
<copy-to-clipboard target="digest" image={ image }/>
|
||||
</td>
|
||||
@@ -93,6 +93,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// window.innerWidth is a blocking access, cache its result.
|
||||
const innerWidth = window.innerWidth;
|
||||
var chars = 0;
|
||||
var max = registryUI.taglist.tags.reduce(function(acc, e) {
|
||||
return e.tag.length > acc ? e.tag.length : acc;
|
||||
}, 0);
|
||||
if (innerWidth >= 1440) {
|
||||
chars = 71;
|
||||
} else if (innerWidth < 1024) {
|
||||
@@ -101,6 +104,7 @@ 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);
|
||||
registryUI.taglist.tags.map(function (image) {
|
||||
image.trigger('content-digest-chars', chars);
|
||||
});
|
||||
@@ -153,8 +157,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
return images;
|
||||
};
|
||||
|
||||
registryUI.taglist.bulkDelete = function() {
|
||||
registryUI.taglist.bulkDelete = function(e) {
|
||||
if (self.multiDelete && self.toDelete > 0) {
|
||||
if (e.altKey) {
|
||||
self._getRemoveImageTags()
|
||||
.filter(function(img) { return img.tags['material-checkbox'].checked; })
|
||||
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
|
||||
}
|
||||
self._getRemoveImageTags().filter(function(img) {
|
||||
return img.tags['material-checkbox'].checked;
|
||||
}).forEach(function(img) {
|
||||
@@ -170,6 +179,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
checkbox._toggle = checkbox.toggle;
|
||||
checkbox.toggle = function(e) {
|
||||
if (e.altKey) {
|
||||
if (!this.checked) { this._toggle(); }
|
||||
self._getRemoveImageTags()
|
||||
.filter(function(img) { return !img.tags['material-checkbox'].checked; })
|
||||
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
|
||||
|
||||
Reference in New Issue
Block a user