2023-11-11 22:18:37 +08:00
|
|
|
<template>
|
|
|
|
<transition name="slide-fade" appear>
|
|
|
|
<div>
|
2024-10-13 18:51:59 +02:00
|
|
|
<h1 v-if="isAdd" class="mb-3">{{$t("compose")}}</h1>
|
2023-12-26 04:12:44 +08:00
|
|
|
<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>
|
2023-11-11 22:18:37 +08:00
|
|
|
|
|
|
|
<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>
|
2023-11-21 18:17:11 +08:00
|
|
|
|
2023-11-25 21:21:23 +08:00
|
|
|
<BDropdown right text="" variant="normal">
|
2023-11-21 18:17:11 +08:00
|
|
|
<BDropdownItem @click="downStack">
|
|
|
|
<font-awesome-icon icon="stop" class="me-1" />
|
|
|
|
{{ $t("downStack") }}
|
|
|
|
</BDropdownItem>
|
|
|
|
</BDropdown>
|
2023-11-11 22:18:37 +08:00
|
|
|
</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>
|
|
|
|
|
2023-11-25 22:14:21 +08:00
|
|
|
<!-- URLs -->
|
|
|
|
<div v-if="urls.length > 0" class="mb-3">
|
|
|
|
<a v-for="(url, index) in urls" :key="index" target="_blank" :href="url.url">
|
|
|
|
<span class="badge bg-secondary me-2">{{ url.display }}</span>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
<!-- Progress Terminal -->
|
|
|
|
<transition name="slide-fade" appear>
|
|
|
|
<Terminal
|
|
|
|
v-show="showProgressTerminal"
|
|
|
|
ref="progressTerminal"
|
|
|
|
class="mb-3 terminal"
|
|
|
|
:name="terminalName"
|
2023-12-26 04:12:44 +08:00
|
|
|
:endpoint="endpoint"
|
2023-11-11 22:18:37 +08:00
|
|
|
: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 -->
|
2023-11-13 18:09:36 +08:00
|
|
|
<div>
|
2023-11-11 22:18:37 +08:00
|
|
|
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
2023-11-13 18:09:36 +08:00
|
|
|
<input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
|
2023-11-19 10:19:33 +01:00
|
|
|
<div class="form-text">{{ $t("Lowercase only") }}</div>
|
2023-11-11 22:18:37 +08:00
|
|
|
</div>
|
2023-12-26 04:12:44 +08:00
|
|
|
|
|
|
|
<!-- 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>
|
2023-11-11 22:18:37 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Containers -->
|
|
|
|
<h4 class="mb-3">{{ $tc("container", 2) }}</h4>
|
|
|
|
|
|
|
|
<div v-if="isEditMode" class="input-group mb-3">
|
|
|
|
<input
|
|
|
|
v-model="newContainerName"
|
2024-10-13 18:51:59 +02:00
|
|
|
:placeholder="$t(`New Container Name...`)"
|
2023-11-11 22:18:37 +08:00
|
|
|
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]"
|
2024-08-08 13:28:55 -04:00
|
|
|
:processing="processing"
|
|
|
|
@start-service="startService"
|
|
|
|
@stop-service="stopService"
|
2024-08-09 10:27:25 -04:00
|
|
|
@restart-service="restartService"
|
2023-11-11 22:18:37 +08:00
|
|
|
/>
|
|
|
|
</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>
|
|
|
|
|
2023-11-25 22:14:21 +08:00
|
|
|
<!-- General -->
|
|
|
|
<div v-if="isEditMode">
|
2023-11-25 22:26:12 +08:00
|
|
|
<h4 class="mb-3">{{ $t("extra") }}</h4>
|
2023-11-25 22:14:21 +08:00
|
|
|
<div class="shadow-box big-padding mb-3">
|
|
|
|
<!-- URLs -->
|
|
|
|
<div class="mb-4">
|
|
|
|
<label class="form-label">
|
|
|
|
{{ $tc("url", 2) }}
|
|
|
|
</label>
|
|
|
|
<ArrayInput name="urls" :display-name="$t('url')" placeholder="https://" object-type="x-dockge" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
<!-- Combined Terminal Output -->
|
|
|
|
<div v-show="!isEditMode">
|
2024-10-13 18:51:59 +02:00
|
|
|
<h4 class="mb-3">{{$t("terminal")}}</h4>
|
2023-11-11 22:18:37 +08:00
|
|
|
<Terminal
|
|
|
|
ref="combinedTerminal"
|
|
|
|
class="mb-3 terminal"
|
|
|
|
:name="combinedTerminalName"
|
2023-12-26 04:12:44 +08:00
|
|
|
:endpoint="endpoint"
|
2023-11-11 22:18:37 +08:00
|
|
|
:rows="combinedTerminalRows"
|
|
|
|
:cols="combinedTerminalCols"
|
2024-10-13 21:11:20 +08:00
|
|
|
style="height: 315px;"
|
2023-11-11 22:18:37 +08:00
|
|
|
></Terminal>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
2023-11-18 06:36:57 +01:00
|
|
|
<h4 class="mb-3">{{ stack.composeFileName }}</h4>
|
2023-11-11 22:18:37 +08:00
|
|
|
|
|
|
|
<!-- 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"
|
2023-12-03 05:12:54 -05:00
|
|
|
:highlight="highlighterYAML"
|
2023-11-11 22:18:37 +08:00
|
|
|
line-numbers :readonly="!isEditMode"
|
|
|
|
@input="yamlCodeChange"
|
|
|
|
@focus="editorFocus = true"
|
|
|
|
@blur="editorFocus = false"
|
|
|
|
></prism-editor>
|
|
|
|
</div>
|
|
|
|
<div v-if="isEditMode" class="mb-3">
|
|
|
|
{{ yamlError }}
|
|
|
|
</div>
|
|
|
|
|
2023-12-03 05:12:54 -05:00
|
|
|
<!-- ENV editor -->
|
|
|
|
<div v-if="isEditMode">
|
|
|
|
<h4 class="mb-3">.env</h4>
|
|
|
|
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
|
|
|
|
<prism-editor
|
|
|
|
ref="editor"
|
|
|
|
v-model="stack.composeENV"
|
|
|
|
class="env-editor"
|
|
|
|
:highlight="highlighterENV"
|
|
|
|
line-numbers :readonly="!isEditMode"
|
|
|
|
@focus="editorFocus = true"
|
|
|
|
@blur="editorFocus = false"
|
|
|
|
></prism-editor>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
<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,
|
2023-12-10 00:59:28 +08:00
|
|
|
copyYAMLComments, envsubstYAML,
|
2023-11-11 22:18:37 +08:00
|
|
|
getCombinedTerminalName,
|
|
|
|
getComposeTerminalName,
|
|
|
|
PROGRESS_TERMINAL_ROWS,
|
|
|
|
RUNNING
|
2023-12-26 04:12:44 +08:00
|
|
|
} from "../../../common/util-common";
|
2023-11-11 22:18:37 +08:00
|
|
|
import { BModal } from "bootstrap-vue-next";
|
|
|
|
import NetworkInput from "../components/NetworkInput.vue";
|
2023-12-10 00:59:28 +08:00
|
|
|
import dotenv from "dotenv";
|
2023-11-11 22:18:37 +08:00
|
|
|
|
2024-05-01 05:20:13 +02:00
|
|
|
const template = `
|
2023-11-11 22:18:37 +08:00
|
|
|
services:
|
|
|
|
nginx:
|
|
|
|
image: nginx:latest
|
|
|
|
restart: unless-stopped
|
|
|
|
ports:
|
|
|
|
- "8080:80"
|
|
|
|
`;
|
2023-12-03 05:12:54 -05:00
|
|
|
const envDefault = "# VARIABLE=value #comment";
|
2023-11-11 22:18:37 +08:00
|
|
|
|
|
|
|
let yamlErrorTimeout = null;
|
|
|
|
|
|
|
|
let serviceStatusTimeout = null;
|
2023-12-03 05:12:54 -05:00
|
|
|
let prismjsSymbolDefinition = {
|
|
|
|
"symbol": {
|
|
|
|
pattern: /(?<!\$)\$(\{[^{}]*\}|\w+)/,
|
|
|
|
}
|
|
|
|
};
|
2023-11-11 22:18:37 +08:00
|
|
|
|
|
|
|
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: {},
|
2023-12-10 00:59:28 +08:00
|
|
|
envsubstJSONConfig: {},
|
2023-11-11 22:18:37 +08:00
|
|
|
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: {
|
2023-11-25 22:14:21 +08:00
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
endpointDisplay() {
|
|
|
|
return this.$root.endpointDisplayFunction(this.endpoint);
|
|
|
|
},
|
|
|
|
|
2023-11-25 22:14:21 +08:00
|
|
|
urls() {
|
2023-12-10 13:17:04 +08:00
|
|
|
if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) {
|
2023-11-25 22:14:21 +08:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
let urls = [];
|
2023-12-10 13:17:04 +08:00
|
|
|
for (const url of this.envsubstJSONConfig["x-dockge"].urls) {
|
2023-11-25 22:14:21 +08:00
|
|
|
let display;
|
|
|
|
try {
|
|
|
|
let obj = new URL(url);
|
|
|
|
let pathname = obj.pathname;
|
|
|
|
if (pathname === "/") {
|
|
|
|
pathname = "";
|
|
|
|
}
|
|
|
|
display = obj.host + pathname + obj.search;
|
|
|
|
} catch (e) {
|
|
|
|
display = url;
|
|
|
|
}
|
|
|
|
|
|
|
|
urls.push({
|
|
|
|
display,
|
|
|
|
url,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return urls;
|
|
|
|
},
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
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() {
|
2023-12-26 04:12:44 +08:00
|
|
|
return this.$root.completeStackList[this.stack.name + "_" + this.endpoint];
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
status() {
|
|
|
|
return this.globalStack?.status;
|
|
|
|
},
|
|
|
|
|
|
|
|
active() {
|
|
|
|
return this.status === RUNNING;
|
|
|
|
},
|
|
|
|
|
|
|
|
terminalName() {
|
|
|
|
if (!this.stack.name) {
|
|
|
|
return "";
|
|
|
|
}
|
2023-12-26 04:12:44 +08:00
|
|
|
return getComposeTerminalName(this.endpoint, this.stack.name);
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
combinedTerminalName() {
|
|
|
|
if (!this.stack.name) {
|
|
|
|
return "";
|
|
|
|
}
|
2023-12-26 04:12:44 +08:00
|
|
|
return getCombinedTerminalName(this.endpoint, this.stack.name);
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
networks() {
|
|
|
|
return this.jsonConfig.networks;
|
2023-12-26 04:12:44 +08:00
|
|
|
},
|
2023-11-11 22:18:37 +08:00
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
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}`;
|
|
|
|
}
|
|
|
|
},
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
"stack.composeYAML": {
|
|
|
|
handler() {
|
|
|
|
if (this.editorFocus) {
|
|
|
|
console.debug("yaml code changed");
|
|
|
|
this.yamlCodeChange();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
deep: true,
|
|
|
|
},
|
2023-12-10 02:29:05 +08:00
|
|
|
|
|
|
|
"stack.composeENV": {
|
|
|
|
handler() {
|
|
|
|
if (this.editorFocus) {
|
|
|
|
console.debug("env code changed");
|
|
|
|
this.yamlCodeChange();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
deep: true,
|
|
|
|
},
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
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,
|
|
|
|
},
|
2023-11-25 02:04:16 +08:00
|
|
|
|
|
|
|
$route(to, from) {
|
2023-12-26 04:12:44 +08:00
|
|
|
|
2023-11-25 02:04:16 +08:00
|
|
|
}
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
if (this.isAdd) {
|
|
|
|
this.processing = false;
|
|
|
|
this.isEditMode = true;
|
|
|
|
|
|
|
|
let composeYAML;
|
2023-12-03 05:12:54 -05:00
|
|
|
let composeENV;
|
2023-11-11 22:18:37 +08:00
|
|
|
|
|
|
|
if (this.$root.composeTemplate) {
|
|
|
|
composeYAML = this.$root.composeTemplate;
|
|
|
|
this.$root.composeTemplate = "";
|
|
|
|
} else {
|
|
|
|
composeYAML = template;
|
|
|
|
}
|
2023-12-03 05:12:54 -05:00
|
|
|
if (this.$root.envTemplate) {
|
|
|
|
composeENV = this.$root.envTemplate;
|
|
|
|
this.$root.envTemplate = "";
|
|
|
|
} else {
|
|
|
|
composeENV = envDefault;
|
|
|
|
}
|
2023-11-11 22:18:37 +08:00
|
|
|
|
|
|
|
// Default Values
|
|
|
|
this.stack = {
|
|
|
|
name: "",
|
|
|
|
composeYAML,
|
2023-12-03 05:12:54 -05:00
|
|
|
composeENV,
|
2023-11-11 22:18:37 +08:00
|
|
|
isManagedByDockge: true,
|
2023-12-26 04:12:44 +08:00
|
|
|
endpoint: "",
|
2023-11-11 22:18:37 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
this.yamlCodeChange();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
this.stack.name = this.$route.params.stackName;
|
|
|
|
this.loadStack();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.requestServiceStatus();
|
|
|
|
},
|
|
|
|
unmounted() {
|
2023-12-26 04:12:44 +08:00
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
startServiceStatusTimeout() {
|
|
|
|
clearTimeout(serviceStatusTimeout);
|
|
|
|
serviceStatusTimeout = setTimeout(async () => {
|
|
|
|
this.requestServiceStatus();
|
2023-11-25 02:04:16 +08:00
|
|
|
}, 5000);
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
requestServiceStatus() {
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "serviceStatusList", this.stack.name, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
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?")) {
|
2023-12-26 04:12:44 +08:00
|
|
|
this.exitAction();
|
2023-11-11 22:18:37 +08:00
|
|
|
next();
|
|
|
|
} else {
|
|
|
|
next(false);
|
|
|
|
}
|
|
|
|
} else {
|
2023-12-26 04:12:44 +08:00
|
|
|
this.exitAction();
|
2023-11-11 22:18:37 +08:00
|
|
|
next();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
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, () => {});
|
|
|
|
},
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
bindTerminal() {
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$refs.progressTerminal?.bind(this.endpoint, this.terminalName);
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
loadStack() {
|
|
|
|
this.processing = true;
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "getStack", this.stack.name, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.bindTerminal();
|
2023-11-11 22:18:37 +08:00
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.stack.endpoint, "deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
this.isEditMode = false;
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$router.push(this.url);
|
2023-11-11 22:18:37 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
saveStack() {
|
|
|
|
this.processing = true;
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.stack.endpoint, "saveStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
this.isEditMode = false;
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$router.push(this.url);
|
2023-11-11 22:18:37 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
startStack() {
|
|
|
|
this.processing = true;
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "startStack", this.stack.name, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
stopStack() {
|
|
|
|
this.processing = true;
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "stopStack", this.stack.name, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2023-11-21 18:17:11 +08:00
|
|
|
downStack() {
|
|
|
|
this.processing = true;
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "downStack", this.stack.name, (res) => {
|
2023-11-21 18:17:11 +08:00
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
restartStack() {
|
|
|
|
this.processing = true;
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "restartStack", this.stack.name, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
updateStack() {
|
|
|
|
this.processing = true;
|
|
|
|
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "updateStack", this.stack.name, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
deleteDialog() {
|
2023-12-26 04:12:44 +08:00
|
|
|
this.$root.emitAgent(this.endpoint, "deleteStack", this.stack.name, (res) => {
|
2023-11-11 22:18:37 +08:00
|
|
|
this.$root.toastRes(res);
|
|
|
|
if (res.ok) {
|
|
|
|
this.$router.push("/");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
discardStack() {
|
|
|
|
this.loadStack();
|
|
|
|
this.isEditMode = false;
|
|
|
|
},
|
|
|
|
|
2023-12-03 05:12:54 -05:00
|
|
|
highlighterYAML(code) {
|
|
|
|
if (!languages.yaml_with_symbols) {
|
|
|
|
languages.yaml_with_symbols = languages.insertBefore("yaml", "punctuation", {
|
|
|
|
"symbol": prismjsSymbolDefinition["symbol"]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return highlight(code, languages.yaml_with_symbols);
|
|
|
|
},
|
|
|
|
|
|
|
|
highlighterENV(code) {
|
|
|
|
if (!languages.docker_env) {
|
|
|
|
languages.docker_env = {
|
|
|
|
"comment": {
|
|
|
|
pattern: /(^#| #).*$/m,
|
|
|
|
greedy: true
|
|
|
|
},
|
|
|
|
"keyword": {
|
2023-12-07 07:14:16 -05:00
|
|
|
pattern: /^\w*(?=[:=])/m,
|
2023-12-03 05:12:54 -05:00
|
|
|
greedy: true
|
|
|
|
},
|
|
|
|
"value": {
|
|
|
|
pattern: /(?<=[:=]).*?((?= #)|$)/m,
|
|
|
|
greedy: true,
|
|
|
|
inside: {
|
|
|
|
"string": [
|
|
|
|
{
|
|
|
|
pattern: /^ *'.*?(?<!\\)'/m,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
pattern: /^ *".*?(?<!\\)"|^.*$/m,
|
|
|
|
inside: prismjsSymbolDefinition
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return highlight(code, languages.docker_env);
|
2023-11-11 22:18:37 +08:00
|
|
|
},
|
|
|
|
|
2023-12-10 00:59:28 +08:00
|
|
|
yamlToJSON(yaml) {
|
|
|
|
let doc = parseDocument(yaml);
|
|
|
|
if (doc.errors.length > 0) {
|
|
|
|
throw doc.errors[0];
|
|
|
|
}
|
2023-11-11 22:18:37 +08:00
|
|
|
|
2023-12-10 00:59:28 +08:00
|
|
|
const config = doc.toJS() ?? {};
|
2023-11-11 22:18:37 +08:00
|
|
|
|
2023-12-10 00:59:28 +08:00
|
|
|
// Check data types
|
|
|
|
// "services" must be an object
|
|
|
|
if (!config.services) {
|
|
|
|
config.services = {};
|
|
|
|
}
|
2023-11-11 22:18:37 +08:00
|
|
|
|
2023-12-10 00:59:28 +08:00
|
|
|
if (Array.isArray(config.services) || typeof config.services !== "object") {
|
|
|
|
throw new Error("Services must be an object");
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
config,
|
|
|
|
doc,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
yamlCodeChange() {
|
|
|
|
try {
|
|
|
|
let { config, doc } = this.yamlToJSON(this.stack.composeYAML);
|
2023-11-11 22:18:37 +08:00
|
|
|
|
|
|
|
this.yamlDoc = doc;
|
|
|
|
this.jsonConfig = config;
|
|
|
|
|
2023-12-10 00:59:28 +08:00
|
|
|
let env = dotenv.parse(this.stack.composeENV);
|
|
|
|
let envYAML = envsubstYAML(this.stack.composeYAML, env);
|
|
|
|
this.envsubstJSONConfig = this.yamlToJSON(envYAML).config;
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
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"
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2023-11-13 18:09:36 +08:00
|
|
|
stackNameToLowercase() {
|
|
|
|
this.stack.name = this.stack?.name?.toLowerCase();
|
|
|
|
},
|
|
|
|
|
2024-08-08 13:28:55 -04:00
|
|
|
startService(serviceName) {
|
|
|
|
this.processing = true;
|
|
|
|
|
|
|
|
this.$root.emitAgent(this.endpoint, "startService", this.stack.name, serviceName, (res) => {
|
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
this.requestServiceStatus(); // Refresh service status
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
stopService(serviceName) {
|
|
|
|
this.processing = true;
|
|
|
|
|
|
|
|
this.$root.emitAgent(this.endpoint, "stopService", this.stack.name, serviceName, (res) => {
|
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
this.requestServiceStatus(); // Refresh service status
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2024-08-09 10:27:25 -04:00
|
|
|
restartService(serviceName) {
|
|
|
|
this.processing = true;
|
|
|
|
|
|
|
|
this.$root.emitAgent(this.endpoint, "restartService", this.stack.name, serviceName, (res) => {
|
|
|
|
this.processing = false;
|
|
|
|
this.$root.toastRes(res);
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
this.requestServiceStatus(); // Refresh service status
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
2023-11-11 22:18:37 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
2023-12-26 04:12:44 +08:00
|
|
|
@import "../styles/vars.scss";
|
|
|
|
|
2023-11-11 22:18:37 +08:00
|
|
|
.terminal {
|
|
|
|
height: 200px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.editor-box {
|
2023-11-12 11:22:31 +03:30
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
font-size: 14px;
|
2023-11-11 22:18:37 +08:00
|
|
|
&.edit-mode {
|
|
|
|
background-color: #2c2f38 !important;
|
|
|
|
}
|
|
|
|
}
|
2023-12-26 04:12:44 +08:00
|
|
|
|
|
|
|
.agent-name {
|
|
|
|
font-size: 13px;
|
|
|
|
color: $dark-font-color3;
|
|
|
|
}
|
2023-11-11 22:18:37 +08:00
|
|
|
</style>
|