mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2025-04-27 23:50:01 +03:00
Add Electron-based Standalone Application (#129)
* add electron app * add some readme * add more documentation * add a password fix for windows * format code * overwrite existing dists * build app first before building electron app * add authentication * add build * use material ui for credentials * add application bar * open dev tools only in dev mode * cleanup code * disable add button if a new item is added * do not always create credentials helper - create it once * improve add button * do not make credential helper modal * use dark mode if user prefers it * disable menubar in credentials window * clean up package json * show windows first when all DOMs are loaded * remove save button * write documentation * load credentials after credentials helper is closed * execute npm install first * add gif animation for the credential helper
This commit is contained in:
parent
f0a40d6087
commit
da9591609e
9 changed files with 558 additions and 1 deletions
|
@ -235,6 +235,10 @@ auth:
|
||||||
path: /etc/docker/registry/htpasswd
|
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](electron/README.md) standalone application.
|
||||||
|
|
||||||
## All examples
|
## All examples
|
||||||
|
|
||||||
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
|
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
|
||||||
|
|
8
electron/.gitignore
vendored
Normal file
8
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
electron/README.md
Normal file
57
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
electron/authentication/index.html
Normal file
8
electron/authentication/index.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
211
electron/authentication/index.tsx
Normal file
211
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
electron/doc/assets/authentication.gif
Normal file
BIN
electron/doc/assets/authentication.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 450 KiB |
229
electron/index.js
Normal file
229
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
electron/package.json
Normal file
39
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/**/* 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"
|
||||||
|
}
|
|
@ -2,7 +2,8 @@
|
||||||
"name": "docker-registry-ui",
|
"name": "docker-registry-ui",
|
||||||
"version": "1.4.8",
|
"version": "1.4.8",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./node_modules/gulp/bin/gulp.js build"
|
"build": "./node_modules/gulp/bin/gulp.js build",
|
||||||
|
"build:electron": "npm run build && cd electron && npm install && npm run dist"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue