mirror of
https://github.com/louislam/dockge.git
synced 2026-05-22 06:22:17 +00:00
Init (#1)
This commit is contained in:
599
frontend/src/pages/Compose.vue
Normal file
599
frontend/src/pages/Compose.vue
Normal file
@@ -0,0 +1,599 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<div v-if="stack.isManagedByDockge" class="mb-3">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button v-if="isEditMode" class="btn btn-primary" :disabled="processing" @click="deployStack">
|
||||
<font-awesome-icon icon="rocket" class="me-1" />
|
||||
{{ $t("deployStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="isEditMode" class="btn btn-normal" :disabled="processing" @click="saveStack">
|
||||
<font-awesome-icon icon="save" class="me-1" />
|
||||
{{ $t("saveStackDraft") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode" class="btn btn-secondary" :disabled="processing" @click="enableEditMode">
|
||||
<font-awesome-icon icon="pen" class="me-1" />
|
||||
{{ $t("editStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode && !active" class="btn btn-primary" :disabled="processing" @click="startStack">
|
||||
<font-awesome-icon icon="play" class="me-1" />
|
||||
{{ $t("startStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode && active" class="btn btn-normal " :disabled="processing" @click="restartStack">
|
||||
<font-awesome-icon icon="rotate" class="me-1" />
|
||||
{{ $t("restartStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="updateStack">
|
||||
<font-awesome-icon icon="cloud-arrow-down" class="me-1" />
|
||||
{{ $t("updateStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode && active" class="btn btn-normal" :disabled="processing" @click="stopStack">
|
||||
<font-awesome-icon icon="stop" class="me-1" />
|
||||
{{ $t("stopStack") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing" @click="showDeleteDialog = !showDeleteDialog">
|
||||
<font-awesome-icon icon="trash" class="me-1" />
|
||||
{{ $t("deleteStack") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Progress Terminal -->
|
||||
<transition name="slide-fade" appear>
|
||||
<Terminal
|
||||
v-show="showProgressTerminal"
|
||||
ref="progressTerminal"
|
||||
class="mb-3 terminal"
|
||||
:name="terminalName"
|
||||
:rows="progressTerminalRows"
|
||||
@has-data="showProgressTerminal = true; submitted = true;"
|
||||
></Terminal>
|
||||
</transition>
|
||||
|
||||
<div v-if="stack.isManagedByDockge" class="row">
|
||||
<div class="col-lg-6">
|
||||
<!-- General -->
|
||||
<div v-if="isAdd">
|
||||
<h4 class="mb-3">{{ $t("general") }}</h4>
|
||||
<div class="shadow-box big-padding mb-3">
|
||||
<!-- Stack Name -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
||||
<input id="name" v-model="stack.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Containers -->
|
||||
<h4 class="mb-3">{{ $tc("container", 2) }}</h4>
|
||||
|
||||
<div v-if="isEditMode" class="input-group mb-3">
|
||||
<input
|
||||
v-model="newContainerName"
|
||||
placeholder="New Container Name..."
|
||||
class="form-control"
|
||||
@keyup.enter="addContainer"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="addContainer">
|
||||
{{ $t("addContainer") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="containerList">
|
||||
<Container
|
||||
v-for="(service, name) in jsonConfig.services"
|
||||
:key="name"
|
||||
:name="name"
|
||||
:is-edit-mode="isEditMode"
|
||||
:first="name === Object.keys(jsonConfig.services)[0]"
|
||||
:status="serviceStatusList[name]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button v-if="false && isEditMode && jsonConfig.services && Object.keys(jsonConfig.services).length > 0" class="btn btn-normal mb-3" @click="addContainer">{{ $t("addContainer") }}</button>
|
||||
|
||||
<!-- Combined Terminal Output -->
|
||||
<div v-show="!isEditMode">
|
||||
<h4 class="mb-3">Terminal</h4>
|
||||
<Terminal
|
||||
ref="combinedTerminal"
|
||||
class="mb-3 terminal"
|
||||
:name="combinedTerminalName"
|
||||
:rows="combinedTerminalRows"
|
||||
:cols="combinedTerminalCols"
|
||||
style="height: 350px;"
|
||||
></Terminal>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<h4 class="mb-3">compose.yaml</h4>
|
||||
|
||||
<!-- YAML editor -->
|
||||
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
|
||||
<prism-editor
|
||||
ref="editor"
|
||||
v-model="stack.composeYAML"
|
||||
class="yaml-editor"
|
||||
:highlight="highlighter"
|
||||
line-numbers :readonly="!isEditMode"
|
||||
@input="yamlCodeChange"
|
||||
@focus="editorFocus = true"
|
||||
@blur="editorFocus = false"
|
||||
></prism-editor>
|
||||
</div>
|
||||
<div v-if="isEditMode" class="mb-3">
|
||||
{{ yamlError }}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- <div class="shadow-box big-padding mb-3">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label"> Search Templates</label>
|
||||
<input id="name" v-model="name" type="text" class="form-control" placeholder="Search..." required>
|
||||
</div>
|
||||
|
||||
<prism-editor v-if="false" v-model="yamlConfig" class="yaml-editor" :highlight="highlighter" line-numbers @input="yamlCodeChange"></prism-editor>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!stack.isManagedByDockge && !processing">
|
||||
{{ $t("stackNotManagedByDockgeMsg") }}
|
||||
</div>
|
||||
|
||||
<!-- Delete Dialog -->
|
||||
<BModal v-model="showDeleteDialog" :okTitle="$t('deleteStack')" okVariant="danger" @ok="deleteDialog">
|
||||
{{ $t("deleteStackMsg") }}
|
||||
</BModal>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "prismjs/components/prism-yaml";
|
||||
import { parseDocument, Document } from "yaml";
|
||||
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
COMBINED_TERMINAL_COLS,
|
||||
COMBINED_TERMINAL_ROWS,
|
||||
copyYAMLComments,
|
||||
getCombinedTerminalName,
|
||||
getComposeTerminalName,
|
||||
PROGRESS_TERMINAL_ROWS,
|
||||
RUNNING
|
||||
} from "../../../backend/util-common";
|
||||
import { BModal } from "bootstrap-vue-next";
|
||||
import NetworkInput from "../components/NetworkInput.vue";
|
||||
|
||||
const template = `version: "3.8"
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
`;
|
||||
|
||||
let yamlErrorTimeout = null;
|
||||
|
||||
let serviceStatusTimeout = null;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NetworkInput,
|
||||
FontAwesomeIcon,
|
||||
PrismEditor,
|
||||
BModal,
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.exitConfirm(next);
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
this.exitConfirm(next);
|
||||
},
|
||||
yamlDoc: null, // For keeping the yaml comments
|
||||
data() {
|
||||
return {
|
||||
editorFocus: false,
|
||||
jsonConfig: {},
|
||||
yamlError: "",
|
||||
processing: true,
|
||||
showProgressTerminal: false,
|
||||
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
|
||||
combinedTerminalRows: COMBINED_TERMINAL_ROWS,
|
||||
combinedTerminalCols: COMBINED_TERMINAL_COLS,
|
||||
stack: {
|
||||
|
||||
},
|
||||
serviceStatusList: {},
|
||||
isEditMode: false,
|
||||
submitted: false,
|
||||
showDeleteDialog: false,
|
||||
newContainerName: "",
|
||||
stopServiceStatusTimeout: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isAdd() {
|
||||
return this.$route.path === "/compose" && !this.submitted;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the stack from the global stack list, because it may contain more real-time data like status
|
||||
* @return {*}
|
||||
*/
|
||||
globalStack() {
|
||||
return this.$root.stackList[this.stack.name];
|
||||
},
|
||||
|
||||
status() {
|
||||
return this.globalStack?.status;
|
||||
},
|
||||
|
||||
active() {
|
||||
return this.status === RUNNING;
|
||||
},
|
||||
|
||||
terminalName() {
|
||||
if (!this.stack.name) {
|
||||
return "";
|
||||
}
|
||||
return getComposeTerminalName(this.stack.name);
|
||||
},
|
||||
|
||||
combinedTerminalName() {
|
||||
if (!this.stack.name) {
|
||||
return "";
|
||||
}
|
||||
return getCombinedTerminalName(this.stack.name);
|
||||
},
|
||||
|
||||
networks() {
|
||||
return this.jsonConfig.networks;
|
||||
}
|
||||
|
||||
},
|
||||
watch: {
|
||||
"stack.composeYAML": {
|
||||
handler() {
|
||||
if (this.editorFocus) {
|
||||
console.debug("yaml code changed");
|
||||
this.yamlCodeChange();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
jsonConfig: {
|
||||
handler() {
|
||||
if (!this.editorFocus) {
|
||||
console.debug("jsonConfig changed");
|
||||
|
||||
let doc = new Document(this.jsonConfig);
|
||||
|
||||
// Stick back the yaml comments
|
||||
if (this.yamlDoc) {
|
||||
copyYAMLComments(doc, this.yamlDoc);
|
||||
}
|
||||
|
||||
this.stack.composeYAML = doc.toString();
|
||||
this.yamlDoc = doc;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isAdd) {
|
||||
this.processing = false;
|
||||
this.isEditMode = true;
|
||||
|
||||
let composeYAML;
|
||||
|
||||
if (this.$root.composeTemplate) {
|
||||
composeYAML = this.$root.composeTemplate;
|
||||
this.$root.composeTemplate = "";
|
||||
|
||||
} else {
|
||||
composeYAML = template;
|
||||
}
|
||||
|
||||
// Default Values
|
||||
this.stack = {
|
||||
name: "",
|
||||
composeYAML,
|
||||
isManagedByDockge: true,
|
||||
};
|
||||
|
||||
this.yamlCodeChange();
|
||||
|
||||
} else {
|
||||
this.stack.name = this.$route.params.stackName;
|
||||
this.loadStack();
|
||||
}
|
||||
|
||||
this.requestServiceStatus();
|
||||
},
|
||||
unmounted() {
|
||||
this.stopServiceStatusTimeout = true;
|
||||
clearTimeout(serviceStatusTimeout);
|
||||
},
|
||||
methods: {
|
||||
|
||||
startServiceStatusTimeout() {
|
||||
clearTimeout(serviceStatusTimeout);
|
||||
serviceStatusTimeout = setTimeout(async () => {
|
||||
this.requestServiceStatus();
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
requestServiceStatus() {
|
||||
this.$root.getSocket().emit("serviceStatusList", this.stack.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.serviceStatusList = res.serviceStatusList;
|
||||
}
|
||||
if (!this.stopServiceStatusTimeout) {
|
||||
this.startServiceStatusTimeout();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
exitConfirm(next) {
|
||||
if (this.isEditMode) {
|
||||
if (confirm("You are currently editing a stack. Are you sure you want to leave?")) {
|
||||
next();
|
||||
} else {
|
||||
next(false);
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
bindTerminal() {
|
||||
this.$refs.progressTerminal?.bind(this.terminalName);
|
||||
},
|
||||
|
||||
loadStack() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.stack = res.stack;
|
||||
this.yamlCodeChange();
|
||||
this.processing = false;
|
||||
this.bindTerminal();
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deployStack() {
|
||||
this.processing = true;
|
||||
|
||||
if (!this.jsonConfig.services) {
|
||||
this.$root.toastError("No services found in compose.yaml");
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if services is object
|
||||
if (typeof this.jsonConfig.services !== "object") {
|
||||
this.$root.toastError("Services must be an object");
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceNameList = Object.keys(this.jsonConfig.services);
|
||||
|
||||
// Set the stack name if empty, use the first container name
|
||||
if (!this.stack.name && serviceNameList.length > 0) {
|
||||
let serviceName = serviceNameList[0];
|
||||
let service = this.jsonConfig.services[serviceName];
|
||||
|
||||
if (service && service.container_name) {
|
||||
this.stack.name = service.container_name;
|
||||
} else {
|
||||
this.stack.name = serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
this.bindTerminal(this.terminalName);
|
||||
|
||||
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.isEditMode = false;
|
||||
this.$router.push("/compose/" + this.stack.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
saveStack() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("saveStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.isEditMode = false;
|
||||
this.$router.push("/compose/" + this.stack.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
startStack() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("startStack", this.stack.name, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
stopStack() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("stopStack", this.stack.name, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
restartStack() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("restartStack", this.stack.name, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
updateStack() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("updateStack", this.stack.name, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
deleteDialog() {
|
||||
this.$root.getSocket().emit("deleteStack", this.stack.name, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
if (res.ok) {
|
||||
this.$router.push("/");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
discardStack() {
|
||||
this.loadStack();
|
||||
this.isEditMode = false;
|
||||
},
|
||||
|
||||
highlighter(code) {
|
||||
return highlight(code, languages.yaml);
|
||||
},
|
||||
|
||||
yamlCodeChange() {
|
||||
try {
|
||||
let doc = parseDocument(this.stack.composeYAML);
|
||||
if (doc.errors.length > 0) {
|
||||
throw doc.errors[0];
|
||||
}
|
||||
|
||||
const config = doc.toJS() ?? {};
|
||||
|
||||
// Check data types
|
||||
// "services" must be an object
|
||||
if (!config.services) {
|
||||
config.services = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(config.services) || typeof config.services !== "object") {
|
||||
throw new Error("Services must be an object");
|
||||
}
|
||||
|
||||
if (!config.version) {
|
||||
config.version = "3.8";
|
||||
}
|
||||
|
||||
this.yamlDoc = doc;
|
||||
this.jsonConfig = config;
|
||||
|
||||
clearTimeout(yamlErrorTimeout);
|
||||
this.yamlError = "";
|
||||
} catch (e) {
|
||||
clearTimeout(yamlErrorTimeout);
|
||||
|
||||
if (this.yamlError) {
|
||||
this.yamlError = e.message;
|
||||
|
||||
} else {
|
||||
yamlErrorTimeout = setTimeout(() => {
|
||||
this.yamlError = e.message;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
enableEditMode() {
|
||||
this.isEditMode = true;
|
||||
},
|
||||
|
||||
checkYAML() {
|
||||
|
||||
},
|
||||
|
||||
addContainer() {
|
||||
this.checkYAML();
|
||||
|
||||
if (this.jsonConfig.services[this.newContainerName]) {
|
||||
this.$root.toastError("Container name already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.newContainerName) {
|
||||
this.$root.toastError("Container name cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsonConfig.services[this.newContainerName] = {
|
||||
restart: "unless-stopped",
|
||||
};
|
||||
this.newContainerName = "";
|
||||
let element = this.$refs.containerList.lastElementChild;
|
||||
element.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth"
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.terminal {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.editor-box {
|
||||
&.edit-mode {
|
||||
background-color: #2c2f38 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user