This commit is contained in:
Louis Lam 2023-11-11 19:14:27 +08:00
parent 2530cac989
commit caa82bbad5
10 changed files with 329 additions and 36 deletions

View File

@ -29,6 +29,7 @@ import { Stack } from "./stack";
import { Cron } from "croner";
import gracefulShutdown from "http-graceful-shutdown";
import User from "./models/user";
import childProcess from "child_process";
export class DockgeServer {
app : Express;
@ -516,6 +517,20 @@ export class DockgeServer {
}
}
getDockerNetworkList() : string[] {
let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]);
let list = res.stdout.toString().split("\n");
// Remove empty string item
list = list.filter((item) => {
return item !== "";
}).sort((a, b) => {
return a.localeCompare(b);
});
return list;
}
get stackDirFullPath() {
return path.resolve(this.stacksDir);
}

View File

@ -203,6 +203,20 @@ export class DockerSocketHandler extends SocketHandler {
callbackError(e, callback);
}
});
// getExternalNetworkList
socket.on("getDockerNetworkList", async (callback) => {
try {
checkLogin(socket);
const dockerNetworkList = server.getDockerNetworkList();
callback({
ok: true,
dockerNetworkList,
});
} catch (e) {
callbackError(e, callback);
}
});
}
saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack {

View File

@ -10,6 +10,7 @@ declare module 'vue' {
About: typeof import('./src/components/settings/About.vue')['default']
Appearance: typeof import('./src/components/settings/Appearance.vue')['default']
ArrayInput: typeof import('./src/components/ArrayInput.vue')['default']
ArraySelect: typeof import('./src/components/ArraySelect.vue')['default']
BModal: typeof import('bootstrap-vue-next')['BModal']
Confirm: typeof import('./src/components/Confirm.vue')['default']
Container: typeof import('./src/components/Container.vue')['default']

View File

@ -0,0 +1,125 @@
<template>
<div>
<div v-if="valid">
<ul v-if="isArrayInited" class="list-group">
<li v-for="(value, index) in array" :key="index" class="list-group-item">
<select v-model="array[index]" class="no-bg domain-input">
<option value="">Select a network...</option>
<option v-for="option in options" :value="option">{{ option }}</option>
</select>
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />
</li>
</ul>
<button class="btn btn-normal btn-sm mt-3" @click="addField">{{ $t("addListItem", [ displayName ]) }}</button>
</div>
<div v-else>
Long syntax is not supported here. Please use the YAML editor.
</div>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true,
},
placeholder: {
type: String,
default: "",
},
displayName: {
type: String,
required: true,
},
options: {
type: Array,
required: true,
},
},
data() {
return {
};
},
computed: {
array() {
// Create the array if not exists, it should be safe.
if (!this.service[this.name]) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.service[this.name] = [];
}
return this.service[this.name];
},
/**
* Check if the array is inited before called v-for.
* Prevent empty arrays inserted to the YAML file.
* @return {boolean}
*/
isArrayInited() {
return this.service[this.name] !== undefined;
},
service() {
return this.$parent.$parent.service;
},
valid() {
// Check if the array is actually an array
if (!Array.isArray(this.array)) {
return false;
}
// Check if the array contains non-object only.
for (let item of this.array) {
if (typeof item === "object") {
return false;
}
}
return true;
}
},
created() {
},
methods: {
addField() {
this.array.push("");
},
remove(index) {
this.array.splice(index, 1);
},
}
};
</script>
<style lang="scss" scoped>
@import "../styles/vars.scss";
.list-group {
background-color: $dark-bg2;
li {
display: flex;
align-items: center;
padding: 10px 0 10px 10px;
.domain-input {
flex-grow: 1;
background-color: $dark-bg2;
border: none;
color: $dark-font-color;
outline: none;
&::placeholder {
color: #1d2634;
}
}
}
}
</style>

View File

@ -114,7 +114,12 @@
<label class="form-label">
{{ $tc("network", 2) }}
</label>
<ArrayInput name="networks" :display-name="$t('network')" placeholder="Network Name" />
<div v-if="networkList.length === 0 && service.networks.length > 0" class="text-warning mb-3">
No networks available. You need to add internal networks or enable external networks in the right side first.
</div>
<ArraySelect name="networks" :display-name="$t('network')" placeholder="Network Name" :options="networkList" />
</div>
<!-- Depends on -->
@ -165,6 +170,14 @@ export default defineComponent({
},
computed: {
networkList() {
let list = [];
for (const networkName in this.jsonObject.networks) {
list.push(networkName);
}
return list;
},
bgStyle() {
if (this.status === "running") {
return "bg-primary";

View File

@ -1,69 +1,157 @@
<template>
<div>
<h5>Internal Networks</h5>
<ul class="list-group">
<li v-for="(networkRow, index) in networkList" :key="index" class="list-group-item">
<input v-model="networkList[index].key" type="text" class="no-bg domain-input" placeholder="Network name..." />
<input v-model="networkRow.key" type="text" class="no-bg domain-input" placeholder="Network name..." />
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />
</li>
</ul>
<button class="btn btn-normal btn-sm mt-3" @click="addField">{{ $t("addNetwork") }}</button>
<button class="btn btn-normal btn-sm mt-3 me-2" @click="addField">{{ $t("addInternalNetwork") }}</button>
<h5 class="mt-3">External Networks</h5>
<div v-if="externalNetworkList.length === 0">
No External Networks
</div>
<div v-for="(networkName, index) in externalNetworkList" :key="networkName" class="form-check form-switch my-3">
<input :id=" 'external-network' + index" v-model="selectedExternalList[networkName]" class="form-check-input" type="checkbox">
<label class="form-check-label" :for=" 'external-network' +index">
{{ networkName }}
</label>
<span v-if="false" class="text-danger ms-2 delete">Delete</span>
</div>
<div v-if="false" class="input-group mb-3">
<input
placeholder="New external network name..."
class="form-control"
@keyup.enter="createExternelNetwork"
/>
<button class="btn btn-normal btn-sm me-2" type="button" @click="">
{{ $t("createExternalNetwork") }}
</button>
</div>
<div v-if="false">
<button class="btn btn-primary btn-sm mt-3 me-2" @click="applyToYAML">{{ $t("applyToYAML") }}</button>
</div>
</div>
</template>
<script>
export default {
props: {
},
data() {
return {
networkList: [],
externalList: {},
selectedExternalList: {},
externalNetworkList: [],
};
},
computed: {
isInited() {
return this.networks !== undefined;
jsonConfig() {
return this.$parent.$parent.jsonConfig;
},
networks() {
return this.$parent.$parent.networks;
stack() {
return this.$parent.$parent.stack;
},
editorFocus() {
return this.$parent.$parent.editorFocus;
},
},
watch: {
networks: {
"jsonConfig.networks": {
handler() {
if (this.editorFocus) {
console.debug("jsonConfig.networks changed");
this.loadNetworkList();
}
},
deep: true,
},
networkList: {
"selectedExternalList": {
handler() {
for (const networkName in this.selectedExternalList) {
const enable = this.selectedExternalList[networkName];
if (enable) {
if (!this.externalList[networkName]) {
this.externalList[networkName] = {};
}
this.externalList[networkName].external = true;
} else {
delete this.externalList[networkName];
}
}
this.applyToYAML();
},
deep: true,
},
"networkList": {
handler() {
this.applyToYAML();
},
deep: true,
}
},
mounted() {
this.loadNetworkList();
this.loadExternalNetworkList();
},
methods: {
loadNetworkList() {
console.debug("loadNetworkList", this.networks);
this.networkList = [];
for (const key in this.networks) {
this.networkList.push({
this.externalList = {};
for (const key in this.jsonConfig.networks) {
let obj = {
key: key,
value: this.networks[key],
});
value: this.jsonConfig.networks[key],
};
if (obj.value && obj.value.external) {
this.externalList[key] = Object.assign({}, obj.value);
} else {
this.networkList.push(obj);
}
}
// Restore selectedExternalList
this.selectedExternalList = {};
for (const networkName in this.externalList) {
this.selectedExternalList[networkName] = true;
}
},
loadExternalNetworkList() {
this.$root.getSocket().emit("getDockerNetworkList", (res) => {
if (res.ok) {
this.externalNetworkList = res.dockerNetworkList.filter((n) => {
// Filter out this stack networks
if (n.startsWith(this.stack.name + "_")) {
return false;
}
// They should be not supported.
// https://docs.docker.com/compose/compose-file/06-networks/#host-or-none
if (n === "none" || n === "host" || n === "bridge") {
return false;
}
return true;
});
} else {
this.$root.toastRes(res);
}
});
},
addField() {
@ -72,10 +160,33 @@ export default {
value: {},
});
},
remove(index) {
this.networkList.splice(index, 1);
this.applyToYAML();
},
applyToYAML() {
if (this.editorFocus) {
return;
}
this.jsonConfig.networks = {};
// Internal networks
for (const networkRow of this.networkList) {
this.jsonConfig.networks[networkRow.key] = networkRow.value;
}
// External networks
for (const networkName in this.externalList) {
this.jsonConfig.networks[networkName] = this.externalList[networkName];
}
console.debug("applyToYAML", this.jsonConfig.networks);
}
},
};
</script>
@ -103,4 +214,10 @@ export default {
}
}
}
.delete {
text-decoration: underline;
font-size: 13px;
cursor: pointer;
}
</style>

View File

@ -45,5 +45,9 @@
"disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
"disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
"passwordNotMatchMsg": "The repeat password does not match.",
"autoGet": "Auto Get"
"autoGet": "Auto Get",
"add": "Add",
"applyToYAML": "Apply to YAML",
"createExternalNetwork": "Create",
"addInternalNetwork": "Add"
}

View File

@ -137,14 +137,18 @@
</div>
<div v-if="isEditMode">
<!-- Volumes -->
<div v-if="false">
<h4 class="mb-3">{{ $tc("volume", 2) }}</h4>
<div class="shadow-box big-padding mb-3">
</div>
</div>
<!-- Networks -->
<h4 class="mb-3">{{ $tc("network", 2) }}</h4>
<div class="shadow-box big-padding mb-3">
<NetworkInput />
</div>
<h4 class="mb-3">{{ $tc("volume", 2) }}</h4>
<div class="shadow-box big-padding mb-3">
</div>
</div>
<!-- <div class="shadow-box big-padding mb-3">

View File

@ -73,7 +73,7 @@
"vue-qrcode": "~2.2.0",
"vue-router": "~4.2.5",
"vue-toastification": "2.0.0-rc.5",
"xterm": "~5.3.0",
"xterm": "~5.4.0-beta.37",
"xterm-addon-web-links": "~0.9.0"
}
}

View File

@ -173,11 +173,11 @@ devDependencies:
specifier: 2.0.0-rc.5
version: 2.0.0-rc.5(vue@3.3.8)
xterm:
specifier: ~5.3.0
version: 5.3.0
specifier: ~5.4.0-beta.37
version: 5.4.0-beta.37
xterm-addon-web-links:
specifier: ~0.9.0
version: 0.9.0(xterm@5.3.0)
version: 0.9.0(xterm@5.4.0-beta.37)
packages:
@ -4406,16 +4406,16 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/xterm-addon-web-links@0.9.0(xterm@5.3.0):
/xterm-addon-web-links@0.9.0(xterm@5.4.0-beta.37):
resolution: {integrity: sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==}
peerDependencies:
xterm: ^5.0.0
dependencies:
xterm: 5.3.0
xterm: 5.4.0-beta.37
dev: true
/xterm@5.3.0:
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
/xterm@5.4.0-beta.37:
resolution: {integrity: sha512-ys+mXqLFrJc7khmYN/MgBnfLv38NgXfkwkEXsCZKHGqn3h2xUBvTvsrSEWO3NQeDPLj4zMr1RwqTblMK9St3BA==}
dev: true
/y18n@4.0.3: