Multiple Dockge instances (#200)

This commit is contained in:
Louis Lam
2023-12-26 04:12:44 +08:00
committed by GitHub
parent 80e885e85d
commit de2de0573b
38 changed files with 1525 additions and 597 deletions

View File

@@ -137,7 +137,7 @@
<script>
import { defineComponent } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { parseDockerPort } from "../../../backend/util-common";
import { parseDockerPort } from "../../../common/util-common";
export default defineComponent({
components: {
@@ -189,14 +189,34 @@ export default defineComponent({
},
terminalRouteLink() {
return {
name: "containerTerminal",
params: {
stackName: this.stackName,
serviceName: this.name,
type: "bash",
},
};
if (this.endpoint) {
return {
name: "containerTerminalEndpoint",
params: {
endpoint: this.endpoint,
stackName: this.stackName,
serviceName: this.name,
type: "bash",
},
};
} else {
return {
name: "containerTerminal",
params: {
stackName: this.stackName,
serviceName: this.name,
type: "bash",
},
};
}
},
endpoint() {
return this.$parent.$parent.endpoint;
},
stack() {
return this.$parent.$parent.stack;
},
stackName() {
@@ -254,8 +274,7 @@ export default defineComponent({
},
methods: {
parsePort(port) {
let hostname = this.$root.info.primaryHostname || location.hostname;
return parseDockerPort(port, hostname);
return parseDockerPort(port, this.stack.primaryHostname);
},
remove() {
delete this.jsonObject.services[this.name];

View File

@@ -65,6 +65,10 @@ export default {
editorFocus() {
return this.$parent.$parent.editorFocus;
},
endpoint() {
return this.$parent.$parent.endpoint;
},
},
watch: {
"jsonConfig.networks": {
@@ -134,7 +138,7 @@ export default {
},
loadExternalNetworkList() {
this.$root.getSocket().emit("getDockerNetworkList", (res) => {
this.$root.emitAgent(this.endpoint, "getDockerNetworkList", (res) => {
if (res.ok) {
this.externalNetworkList = res.dockerNetworkList.filter((n) => {
// Filter out this stack networks

View File

@@ -43,7 +43,7 @@
</div>
</div>
<div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle">
<div v-if="Object.keys($root.stackList).length === 0" class="text-center mt-3">
<div v-if="Object.keys(sortedStackList).length === 0" class="text-center mt-3">
<router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link>
</div>
@@ -67,7 +67,7 @@
<script>
import Confirm from "../components/Confirm.vue";
import StackListItem from "../components/StackListItem.vue";
import { CREATED_FILE, CREATED_STACK, EXITED, RUNNING, UNKNOWN } from "../../../backend/util-common";
import { CREATED_FILE, CREATED_STACK, EXITED, RUNNING, UNKNOWN } from "../../../common/util-common";
export default {
components: {
@@ -120,7 +120,7 @@ export default {
* @returns {Array} The sorted list of stacks.
*/
sortedStackList() {
let result = Object.values(this.$root.stackList);
let result = Object.values(this.$root.completeStackList);
result = result.filter(stack => {
// filter by search text
@@ -160,6 +160,7 @@ export default {
return 1;
}
// sort by status
if (m1.status !== m2.status) {
if (m2.status === RUNNING) {
return 1;

View File

@@ -1,12 +1,14 @@
<template>
<router-link :to="`/compose/${stack.name}`" :class="{ 'dim' : !stack.isManagedByDockge }" class="item">
<router-link :to="url" :class="{ 'dim' : !stack.isManagedByDockge }" class="item">
<Uptime :stack="stack" :fixed-width="true" class="me-2" />
<span class="title">{{ stackName }}</span>
<div class="title">
<span>{{ stackName }}</span>
<div v-if="$root.agentCount > 1" class="endpoint">{{ endpointDisplay }}</div>
</div>
</router-link>
</template>
<script>
import Uptime from "./Uptime.vue";
export default {
@@ -51,6 +53,16 @@ export default {
};
},
computed: {
endpointDisplay() {
return this.$root.endpointDisplayFunction(this.stack.endpoint);
},
url() {
if (this.stack.endpoint) {
return `/compose/${this.stack.name}/${this.stack.endpoint}`;
} else {
return `/compose/${this.stack.name}`;
}
},
depthMargin() {
return {
marginLeft: `${31 * this.depth}px`,
@@ -117,16 +129,31 @@ export default {
padding-right: 2px !important;
}
// .stack-item {
// width: 100%;
// }
.tags {
margin-top: 4px;
padding-left: 67px;
.item {
text-decoration: none;
display: flex;
flex-wrap: wrap;
gap: 0;
align-items: center;
min-height: 52px;
border-radius: 10px;
transition: all ease-in-out 0.15s;
width: 100%;
padding: 5px 8px;
&.disabled {
opacity: 0.3;
}
&:hover {
background-color: $highlight-white;
}
&.active {
background-color: #cdf8f4;
}
.title {
margin-top: -4px;
}
.endpoint {
font-size: 12px;
color: $dark-font-color3;
}
}
.collapsed {

View File

@@ -7,8 +7,7 @@
<script>
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "xterm-addon-web-links";
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../backend/util-common";
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../common/util-common";
export default {
/**
@@ -24,6 +23,11 @@ export default {
require: true,
},
endpoint: {
type: String,
require: true,
},
// Require if mode is interactive
stackName: {
type: String,
@@ -110,14 +114,14 @@ export default {
// Create a new Terminal
if (this.mode === "mainTerminal") {
this.$root.getSocket().emit("mainTerminal", this.name, (res) => {
this.$root.emitAgent(this.endpoint, "mainTerminal", this.name, (res) => {
if (!res.ok) {
this.$root.toastRes(res);
}
});
} else if (this.mode === "interactive") {
console.debug("Create Interactive terminal:", this.name);
this.$root.getSocket().emit("interactiveTerminal", this.stackName, this.serviceName, this.shell, (res) => {
this.$root.emitAgent(this.endpoint, "interactiveTerminal", this.stackName, this.serviceName, this.shell, (res) => {
if (!res.ok) {
this.$root.toastRes(res);
}
@@ -134,15 +138,15 @@ export default {
},
methods: {
bind(name) {
bind(endpoint, name) {
// Workaround: normally this.name should be set, but it is not sometimes, so we use the parameter, but eventually this.name and name must be the same name
if (name) {
this.$root.unbindTerminal(name);
this.$root.bindTerminal(name, this.terminal);
this.$root.bindTerminal(endpoint, name, this.terminal);
console.debug("Terminal bound via parameter: " + name);
} else if (this.name) {
this.$root.unbindTerminal(this.name);
this.$root.bindTerminal(this.name, this.terminal);
this.$root.bindTerminal(this.endpoint, this.name, this.terminal);
console.debug("Terminal bound: " + this.name);
} else {
console.debug("Terminal name not set");
@@ -173,7 +177,7 @@ export default {
// Remove the input from the terminal
this.removeInput();
this.$root.getSocket().emit("terminalInput", this.name, buffer + e.key, (err) => {
this.$root.emitAgent(this.endpoint, "terminalInput", this.name, buffer + e.key, (err) => {
this.$root.toastError(err.msg);
});
@@ -192,7 +196,7 @@ export default {
// TODO
} else if (e.key === "\u0003") { // Ctrl + C
console.debug("Ctrl + C");
this.$root.getSocket().emit("terminalInput", this.name, e.key);
this.$root.emitAgent(this.endpoint, "terminalInput", this.name, e.key);
this.removeInput();
} else {
this.cursorPosition++;
@@ -205,7 +209,7 @@ export default {
interactiveTerminalConfig() {
this.terminal.onKey(e => {
this.$root.getSocket().emit("terminalInput", this.name, e.key, (res) => {
this.$root.emitAgent(this.endpoint, "terminalInput", this.name, e.key, (res) => {
if (!res.ok) {
this.$root.toastRes(res);
}
@@ -234,7 +238,7 @@ export default {
this.terminalFitAddOn.fit();
let rows = this.terminal.rows;
let cols = this.terminal.cols;
this.$root.getSocket().emit("terminalResize", this.name, rows, cols);
this.$root.emitAgent(this.endpoint, "terminalResize", this.name, rows, cols);
}
}
};

View File

@@ -3,7 +3,7 @@
</template>
<script>
import { statusColor, statusNameShort } from "../../../backend/util-common";
import { statusColor, statusNameShort } from "../../../common/util-common";
export default {
props: {

View File

@@ -99,5 +99,17 @@
"connecting...": "Connecting to the socket server…",
"url": "URL | URLs",
"extra": "Extra",
"newUpdate": "New Update"
"newUpdate": "New Update",
"dockgeAgent": "Dockge Agent | Dockge Agents",
"currentEndpoint": "Current",
"dockgeURL": "Dockge URL (e.g. http://127.0.0.1:5001)",
"agentOnline": "Online",
"agentOffline": "Offline",
"connecting": "Connecting",
"connect": "Connect",
"addAgent": "Add Agent",
"agentAddedSuccessfully": "Agent added successfully.",
"agentRemovedSuccessfully": "Agent removed successfully.",
"removeAgent": "Remove Agent",
"removeAgentMsg": "Are you sure you want to remove this agent?"
}

View File

@@ -98,6 +98,7 @@
<script>
import Login from "../components/Login.vue";
import { compareVersions } from "compare-versions";
import { ALL_ENDPOINTS } from "../../../common/util-common";
export default {
@@ -145,7 +146,7 @@ export default {
methods: {
scanFolder() {
this.$root.getSocket().emit("requestStackList", (res) => {
this.$root.emitAgent(ALL_ENDPOINTS, "requestStackList", (res) => {
this.$root.toastRes(res);
});
},

View File

@@ -1,5 +1,5 @@
// Dayjs init inside this, so it has to be the first import
import "../../backend/util-common";
import "../../common/util-common";
import { createApp, defineComponent, h } from "vue";
import App from "./App.vue";

View File

@@ -3,6 +3,7 @@ import { Socket } from "socket.io-client";
import { defineComponent } from "vue";
import jwtDecode from "jwt-decode";
import { Terminal } from "@xterm/xterm";
import { AgentSocket } from "../../../common/agent-socket";
let socket : Socket;
@@ -28,16 +29,51 @@ export default defineComponent({
loggedIn: false,
allowLoginDialog: false,
username: null,
stackList: {},
composeTemplate: "",
stackList: {},
// All stack list from all agents
allAgentStackList: {} as Record<string, object>,
// online / offline / connecting
agentStatusList: {
},
// Agent List
agentList: {
},
};
},
computed: {
agentCount() {
return Object.keys(this.agentList).length;
},
completeStackList() {
let list : Record<string, object> = {};
for (let stackName in this.stackList) {
list[stackName + "_"] = this.stackList[stackName];
}
for (let endpoint in this.allAgentStackList) {
let instance = this.allAgentStackList[endpoint];
for (let stackName in instance.stackList) {
list[stackName + "_" + endpoint] = instance.stackList[stackName];
}
}
return list;
},
usernameFirstChar() {
if (typeof this.username == "string" && this.username.length >= 1) {
return this.username.charAt(0).toUpperCase();
} else {
return "🐻";
return "🐬";
}
},
@@ -65,6 +101,15 @@ export default defineComponent({
},
watch: {
"socketIO.connected"() {
if (this.socketIO.connected) {
this.agentStatusList[""] = "online";
} else {
this.agentStatusList[""] = "offline";
}
},
remember() {
localStorage.remember = (this.remember) ? "1" : "0";
},
@@ -84,6 +129,15 @@ export default defineComponent({
},
methods: {
endpointDisplayFunction(endpoint : string) {
if (endpoint) {
return endpoint;
} else {
return this.$t("currentEndpoint");
}
},
/**
* Initialize connection to socket server
* @param bypass Should the check for if we
@@ -108,8 +162,12 @@ export default defineComponent({
this.socketIO.connecting = true;
}, 1500);
socket = io(url, {
transports: [ "websocket", "polling" ]
socket = io(url);
// Handling events from agents
let agentSocket = new AgentSocket();
socket.on("agent", (eventName : unknown, ...args : unknown[]) => {
agentSocket.call(eventName, ...args);
});
socket.on("connect", () => {
@@ -177,7 +235,7 @@ export default defineComponent({
this.$router.push("/setup");
});
socket.on("terminalWrite", (terminalName, data) => {
agentSocket.on("terminalWrite", (terminalName, data) => {
const terminal = terminalMap.get(terminalName);
if (!terminal) {
//console.error("Terminal not found: " + terminalName);
@@ -186,9 +244,18 @@ export default defineComponent({
terminal.write(data);
});
socket.on("stackList", (res) => {
agentSocket.on("stackList", (res) => {
if (res.ok) {
this.stackList = res.stackList;
if (!res.endpoint) {
this.stackList = res.stackList;
} else {
if (!this.allAgentStackList[res.endpoint]) {
this.allAgentStackList[res.endpoint] = {
stackList: {},
};
}
this.allAgentStackList[res.endpoint].stackList = res.stackList;
}
}
});
@@ -203,6 +270,21 @@ export default defineComponent({
}
});
socket.on("agentStatus", (res) => {
this.agentStatusList[res.endpoint] = res.status;
if (res.msg) {
this.toastError(res.msg);
}
});
socket.on("agentList", (res) => {
console.log(res);
if (res.ok) {
this.agentList = res.agentList;
}
});
socket.on("refresh", () => {
location.reload();
});
@@ -220,6 +302,10 @@ export default defineComponent({
return socket;
},
emitAgent(endpoint : string, eventName : string, ...args : unknown[]) {
this.getSocket().emit("agent", endpoint, eventName, ...args);
},
/**
* Get payload of JWT cookie
* @returns {(object | undefined)} JWT payload
@@ -310,9 +396,9 @@ export default defineComponent({
},
bindTerminal(terminalName : string, terminal : Terminal) {
bindTerminal(endpoint : string, terminalName : string, terminal : Terminal) {
// Load terminal, get terminal screen
socket.emit("terminalJoin", terminalName, (res) => {
this.emitAgent(endpoint, "terminalJoin", terminalName, (res) => {
if (res.ok) {
terminal.write(res.buffer);
terminalMap.set(terminalName, terminal);

View File

@@ -2,7 +2,12 @@
<transition name="slide-fade" appear>
<div>
<h1 v-if="isAdd" class="mb-3">Compose</h1>
<h1 v-else class="mb-3"><Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}</h1>
<h1 v-else class="mb-3">
<Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}
<span v-if="$root.agentCount > 1" class="agent-name">
({{ endpointDisplay }})
</span>
</h1>
<div v-if="stack.isManagedByDockge" class="mb-3">
<div class="btn-group me-2" role="group">
@@ -70,6 +75,7 @@
ref="progressTerminal"
class="mb-3 terminal"
:name="terminalName"
:endpoint="endpoint"
:rows="progressTerminalRows"
@has-data="showProgressTerminal = true; submitted = true;"
></Terminal>
@@ -87,6 +93,16 @@
<input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
<div class="form-text">{{ $t("Lowercase only") }}</div>
</div>
<!-- Endpoint -->
<div class="mt-3">
<label for="name" class="form-label">{{ $t("dockgeAgent") }}</label>
<select v-model="stack.endpoint" class="form-select">
<option v-for="(agent, endpoint) in $root.agentList" :key="endpoint" :value="endpoint" :disabled="$root.agentStatusList[endpoint] != 'online'">
({{ $root.agentStatusList[endpoint] }}) {{ (endpoint) ? endpoint : $t("currentEndpoint") }}
</option>
</select>
</div>
</div>
</div>
@@ -139,6 +155,7 @@
ref="combinedTerminal"
class="mb-3 terminal"
:name="combinedTerminalName"
:endpoint="endpoint"
:rows="combinedTerminalRows"
:cols="combinedTerminalCols"
style="height: 350px;"
@@ -236,7 +253,7 @@ import {
getComposeTerminalName,
PROGRESS_TERMINAL_ROWS,
RUNNING
} from "../../../backend/util-common";
} from "../../../common/util-common";
import { BModal } from "bootstrap-vue-next";
import NetworkInput from "../components/NetworkInput.vue";
import dotenv from "dotenv";
@@ -298,6 +315,10 @@ export default {
},
computed: {
endpointDisplay() {
return this.$root.endpointDisplayFunction(this.endpoint);
},
urls() {
if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) {
return [];
@@ -334,7 +355,7 @@ export default {
* @return {*}
*/
globalStack() {
return this.$root.stackList[this.stack.name];
return this.$root.completeStackList[this.stack.name + "_" + this.endpoint];
},
status() {
@@ -349,20 +370,31 @@ export default {
if (!this.stack.name) {
return "";
}
return getComposeTerminalName(this.stack.name);
return getComposeTerminalName(this.endpoint, this.stack.name);
},
combinedTerminalName() {
if (!this.stack.name) {
return "";
}
return getCombinedTerminalName(this.stack.name);
return getCombinedTerminalName(this.endpoint, this.stack.name);
},
networks() {
return this.jsonConfig.networks;
}
},
endpoint() {
return this.stack.endpoint || this.$route.params.endpoint || "";
},
url() {
if (this.stack.endpoint) {
return `/compose/${this.stack.name}/${this.stack.endpoint}`;
} else {
return `/compose/${this.stack.name}`;
}
},
},
watch: {
"stack.composeYAML": {
@@ -405,9 +437,7 @@ export default {
},
$route(to, from) {
// Leave Combined Terminal
console.debug("leaveCombinedTerminal", from.params.stackName);
this.$root.getSocket().emit("leaveCombinedTerminal", this.stack.name, () => {});
}
},
mounted() {
@@ -437,6 +467,7 @@ export default {
composeYAML,
composeENV,
isManagedByDockge: true,
endpoint: "",
};
this.yamlCodeChange();
@@ -449,11 +480,9 @@ export default {
this.requestServiceStatus();
},
unmounted() {
this.stopServiceStatusTimeout = true;
clearTimeout(serviceStatusTimeout);
},
methods: {
startServiceStatusTimeout() {
clearTimeout(serviceStatusTimeout);
serviceStatusTimeout = setTimeout(async () => {
@@ -462,7 +491,7 @@ export default {
},
requestServiceStatus() {
this.$root.getSocket().emit("serviceStatusList", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "serviceStatusList", this.stack.name, (res) => {
if (res.ok) {
this.serviceStatusList = res.serviceStatusList;
}
@@ -475,22 +504,34 @@ export default {
exitConfirm(next) {
if (this.isEditMode) {
if (confirm("You are currently editing a stack. Are you sure you want to leave?")) {
this.exitAction();
next();
} else {
next(false);
}
} else {
this.exitAction();
next();
}
},
exitAction() {
console.log("exitAction");
this.stopServiceStatusTimeout = true;
clearTimeout(serviceStatusTimeout);
// Leave Combined Terminal
console.debug("leaveCombinedTerminal", this.endpoint, this.stack.name);
this.$root.emitAgent(this.endpoint, "leaveCombinedTerminal", this.stack.name, () => {});
},
bindTerminal() {
this.$refs.progressTerminal?.bind(this.terminalName);
this.$refs.progressTerminal?.bind(this.endpoint, this.terminalName);
},
loadStack() {
this.processing = true;
this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "getStack", this.stack.name, (res) => {
if (res.ok) {
this.stack = res.stack;
this.yamlCodeChange();
@@ -532,15 +573,15 @@ export default {
}
}
this.bindTerminal(this.terminalName);
this.bindTerminal();
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
this.$root.emitAgent(this.stack.endpoint, "deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
this.processing = false;
this.$root.toastRes(res);
if (res.ok) {
this.isEditMode = false;
this.$router.push("/compose/" + this.stack.name);
this.$router.push(this.url);
}
});
},
@@ -548,13 +589,13 @@ export default {
saveStack() {
this.processing = true;
this.$root.getSocket().emit("saveStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
this.$root.emitAgent(this.stack.endpoint, "saveStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
this.processing = false;
this.$root.toastRes(res);
if (res.ok) {
this.isEditMode = false;
this.$router.push("/compose/" + this.stack.name);
this.$router.push(this.url);
}
});
},
@@ -562,7 +603,7 @@ export default {
startStack() {
this.processing = true;
this.$root.getSocket().emit("startStack", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "startStack", this.stack.name, (res) => {
this.processing = false;
this.$root.toastRes(res);
});
@@ -571,7 +612,7 @@ export default {
stopStack() {
this.processing = true;
this.$root.getSocket().emit("stopStack", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "stopStack", this.stack.name, (res) => {
this.processing = false;
this.$root.toastRes(res);
});
@@ -580,7 +621,7 @@ export default {
downStack() {
this.processing = true;
this.$root.getSocket().emit("downStack", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "downStack", this.stack.name, (res) => {
this.processing = false;
this.$root.toastRes(res);
});
@@ -589,7 +630,7 @@ export default {
restartStack() {
this.processing = true;
this.$root.getSocket().emit("restartStack", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "restartStack", this.stack.name, (res) => {
this.processing = false;
this.$root.toastRes(res);
});
@@ -598,14 +639,14 @@ export default {
updateStack() {
this.processing = true;
this.$root.getSocket().emit("updateStack", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "updateStack", this.stack.name, (res) => {
this.processing = false;
this.$root.toastRes(res);
});
},
deleteDialog() {
this.$root.getSocket().emit("deleteStack", this.stack.name, (res) => {
this.$root.emitAgent(this.endpoint, "deleteStack", this.stack.name, (res) => {
this.$root.toastRes(res);
if (res.ok) {
this.$router.push("/");
@@ -750,6 +791,8 @@ export default {
</script>
<style scoped lang="scss">
@import "../styles/vars.scss";
.terminal {
height: 200px;
}
@@ -761,4 +804,9 @@ export default {
background-color: #2c2f38 !important;
}
}
.agent-name {
font-size: 13px;
color: $dark-font-color3;
}
</style>

View File

@@ -15,14 +15,14 @@
</p>
</div>
<Terminal class="terminal" :rows="20" mode="mainTerminal" name="console"></Terminal>
<Terminal class="terminal" :rows="20" mode="mainTerminal" name="console" :endpoint="endpoint"></Terminal>
</div>
</transition>
</template>
<script>
import { allowedCommandList } from "../../../backend/util-common";
import { allowedCommandList } from "../../../common/util-common";
export default {
components: {
@@ -32,6 +32,11 @@ export default {
allowedCommandList,
};
},
computed: {
endpoint() {
return this.$route.params.endpoint || "";
},
},
mounted() {
},

View File

@@ -7,13 +7,13 @@
<router-link :to="sh" class="btn btn-normal me-2">Switch to sh</router-link>
</div>
<Terminal class="terminal" :rows="20" mode="interactive" :name="terminalName" :stack-name="stackName" :service-name="serviceName" :shell="shell"></Terminal>
<Terminal class="terminal" :rows="20" mode="interactive" :name="terminalName" :stack-name="stackName" :service-name="serviceName" :shell="shell" :endpoint="endpoint"></Terminal>
</div>
</transition>
</template>
<script>
import { getContainerExecTerminalName } from "../../../backend/util-common";
import { getContainerExecTerminalName } from "../../../common/util-common";
export default {
components: {
@@ -27,6 +27,9 @@ export default {
stackName() {
return this.$route.params.stackName;
},
endpoint() {
return this.$route.params.endpoint || "";
},
shell() {
return this.$route.params.type;
},
@@ -34,10 +37,12 @@ export default {
return this.$route.params.serviceName;
},
terminalName() {
return getContainerExecTerminalName(this.stackName, this.serviceName, 0);
return getContainerExecTerminalName(this.endpoint, this.stackName, this.serviceName, 0);
},
sh() {
return {
let endpoint = this.$route.params.endpoint;
let data = {
name: "containerTerminal",
params: {
stackName: this.stackName,
@@ -45,6 +50,13 @@ export default {
type: "sh",
},
};
if (endpoint) {
data.name = "containerTerminalEndpoint";
data.params.endpoint = endpoint;
}
return data;
},
},
mounted() {

View File

@@ -5,36 +5,97 @@
{{ $t("home") }}
</h1>
<div class="shadow-box big-padding text-center mb-4">
<div class="row">
<div class="col">
<h3>{{ $t("active") }}</h3>
<span class="num active">{{ activeNum }}</span>
<div class="row first-row">
<!-- Left -->
<div class="col-md-7">
<!-- Stats -->
<div class="shadow-box big-padding text-center mb-4">
<div class="row">
<div class="col">
<h3>{{ $t("active") }}</h3>
<span class="num active">{{ activeNum }}</span>
</div>
<div class="col">
<h3>{{ $t("exited") }}</h3>
<span class="num exited">{{ exitedNum }}</span>
</div>
<div class="col">
<h3>{{ $t("inactive") }}</h3>
<span class="num inactive">{{ inactiveNum }}</span>
</div>
</div>
</div>
<div class="col">
<h3>{{ $t("exited") }}</h3>
<span class="num exited">{{ exitedNum }}</span>
<!-- Docker Run -->
<h2 class="mb-3">{{ $t("Docker Run") }}</h2>
<div class="mb-3">
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
</div>
<div class="col">
<h3>{{ $t("inactive") }}</h3>
<span class="num inactive">{{ inactiveNum }}</span>
<button class="btn-normal btn mb-4" @click="convertDockerRun">{{ $t("Convert to Compose") }}</button>
</div>
<!-- Right -->
<div class="col-md-5">
<!-- Agent List -->
<div class="shadow-box big-padding">
<h4 class="mb-3">{{ $tc("dockgeAgent", 2) }} <span class="badge bg-warning" style="font-size: 12px;">beta</span></h4>
<div v-for="(agent, endpoint) in $root.agentList" :key="endpoint" class="mb-3 agent">
<!-- Agent Status -->
<template v-if="$root.agentStatusList[endpoint]">
<span v-if="$root.agentStatusList[endpoint] === 'online'" class="badge bg-primary me-2">{{ $t("agentOnline") }}</span>
<span v-else-if="$root.agentStatusList[endpoint] === 'offline'" class="badge bg-danger me-2">{{ $t("agentOffline") }}</span>
<span v-else class="badge bg-secondary me-2">{{ $t($root.agentStatusList[endpoint]) }}</span>
</template>
<!-- Agent Display Name -->
<span v-if="endpoint === ''">{{ $t("currentEndpoint") }}</span>
<a v-else :href="agent.url" target="_blank">{{ endpoint }}</a>
<!-- Remove Button -->
<font-awesome-icon v-if="endpoint !== ''" class="ms-2 remove-agent" icon="trash" @click="showRemoveAgentDialog[agent.url] = !showRemoveAgentDialog[agent.url]" />
<!-- Remoe Agent Dialog -->
<BModal v-model="showRemoveAgentDialog[agent.url]" :okTitle="$t('removeAgent')" okVariant="danger" @ok="removeAgent(agent.url)">
<p>{{ agent.url }}</p>
{{ $t("removeAgentMsg") }}
</BModal>
</div>
<button v-if="!showAgentForm" class="btn btn-normal" @click="showAgentForm = !showAgentForm">{{ $t("addAgent") }}</button>
<!-- Add Agent Form -->
<form v-if="showAgentForm" @submit.prevent="addAgent">
<div class="mb-3">
<label for="url" class="form-label">{{ $t("dockgeURL") }}</label>
<input id="url" v-model="agent.url" type="url" class="form-control" required placeholder="http://">
</div>
<div class="mb-3">
<label for="username" class="form-label">{{ $t("Username") }}</label>
<input id="username" v-model="agent.username" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ $t("Password") }}</label>
<input id="password" v-model="agent.password" type="password" class="form-control" required autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary" :disabled="connectingAgent">
<template v-if="connectingAgent">{{ $t("connecting") }}</template>
<template v-else>{{ $t("connect") }}</template>
</button>
</form>
</div>
</div>
</div>
<h2 class="mb-3">{{ $t("Docker Run") }}</h2>
<div class="mb-3">
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
</div>
<button class="btn-normal btn" @click="convertDockerRun">{{ $t("Convert to Compose") }}</button>
</div>
</transition>
<router-view ref="child" />
</template>
<script>
import { statusNameShort } from "../../../backend/util-common";
import { statusNameShort } from "../../../common/util-common";
export default {
components: {
@@ -58,6 +119,14 @@ export default {
importantHeartBeatListLength: 0,
displayedRecords: [],
dockerRunCommand: "",
showAgentForm: false,
showRemoveAgentDialog: {},
connectingAgent: false,
agent: {
url: "http://",
username: "",
password: "",
}
};
},
@@ -98,11 +167,43 @@ export default {
methods: {
addAgent() {
this.connectingAgent = true;
this.$root.getSocket().emit("addAgent", this.agent, (res) => {
this.$root.toastRes(res);
if (res.ok) {
this.showAgentForm = false;
this.agent = {
url: "http://",
username: "",
password: "",
};
}
this.connectingAgent = false;
});
},
removeAgent(url) {
this.$root.getSocket().emit("removeAgent", url, (res) => {
if (res.ok) {
this.$root.toastRes(res);
let urlObj = new URL(url);
let endpoint = urlObj.host;
// Remove the stack list and status list of the removed agent
delete this.$root.allAgentStackList[endpoint];
}
});
},
getStatusNum(statusName) {
let num = 0;
for (let stackName in this.$root.stackList) {
const stack = this.$root.stackList[stackName];
for (let stackName in this.$root.completeStackList) {
const stack = this.$root.completeStackList[stackName];
if (statusNameShort(stack.status) === statusName) {
num += 1;
}
@@ -230,4 +331,20 @@ table {
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
}
.first-row .shadow-box {
}
.remove-agent {
cursor: pointer;
color: rgba(255, 255, 255, 0.3);
}
.agent {
a {
text-decoration: none;
}
}
</style>

View File

@@ -35,22 +35,33 @@ const routes = [
component: Compose,
},
{
path: "/compose/:stackName",
name: "compose",
path: "/compose/:stackName/:endpoint",
component: Compose,
},
{
path: "/compose/:stackName",
component: Compose,
props: true,
},
{
path: "/terminal/:stackName/:serviceName/:type",
component: ContainerTerminal,
name: "containerTerminal",
},
{
path: "/terminal/:stackName/:serviceName/:type/:endpoint",
component: ContainerTerminal,
name: "containerTerminalEndpoint",
},
]
},
{
path: "/console",
component: Console,
},
{
path: "/console/:endpoint",
component: Console,
},
{
path: "/settings",
component: Settings,