dockge/frontend/src/pages/DashboardHome.vue

384 lines
13 KiB
Vue
Raw Normal View History

2023-11-11 22:18:37 +08:00
<template>
<transition ref="tableContainer" name="slide-fade" appear>
<div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">
{{ $t("home") }}
</h1>
2023-12-26 04:12:44 +08:00
<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>
2023-11-11 22:18:37 +08:00
</div>
2023-12-26 04:12:44 +08:00
<!-- 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>
2023-11-11 22:18:37 +08:00
</div>
2023-12-26 04:12:44 +08:00
<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 -->
<template v-if="$root.agentStatusList[endpoint]">
<span v-if="endpoint === '' && agent.name === ''" class="badge bg-secondary me-2">Controller</span>
<span v-else-if="agent.name === ''" :href="agent.url" class="me-2">{{ endpoint }}</span>
<span v-else :href="agent.url" class="me-2">{{ agent.name }}</span>
</template>
2023-12-26 04:12:44 +08:00
<!-- Edit Name -->
<font-awesome-icon icon="pen-to-square" @click="showEditAgentNameDialog[agent.name] = !showEditAgentNameDialog[agent.Name]" />
<!-- Edit Dialog -->
<BModal v-model="showEditAgentNameDialog[agent.name]" :no-close-on-backdrop="true" :close-on-esc="true" :okTitle="$t('Update Name')" okVariant="info" @ok="updateName(agent.url, agent.updatedName)">
<label for="Update Name" class="form-label">Current value: {{ $t(agent.name) }}</label>
<input id="updatedName" v-model="agent.updatedName" type="text" class="form-control" optional>
</BModal>
2023-12-26 04:12:44 +08:00
<!-- Remove Button -->
<font-awesome-icon v-if="endpoint !== ''" class="ms-2 remove-agent" icon="trash" @click="showRemoveAgentDialog[agent.url] = !showRemoveAgentDialog[agent.url]" />
<!-- Remove Agent Dialog -->
2023-12-26 04:12:44 +08:00
<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>
<div class="mb-3">
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="name" v-model="agent.name" type="text" class="form-control" optional>
</div>
2023-12-26 04:12:44 +08:00
<button type="submit" class="btn btn-primary" :disabled="connectingAgent">
<template v-if="connectingAgent">{{ $t("connecting") }}</template>
<template v-else>{{ $t("connect") }}</template>
</button>
</form>
2023-11-11 22:18:37 +08:00
</div>
</div>
</div>
</div>
</transition>
<router-view ref="child" />
</template>
<script>
2023-12-26 04:12:44 +08:00
import { statusNameShort } from "../../../common/util-common";
2023-11-11 22:18:37 +08:00
export default {
components: {
},
props: {
calculatedHeight: {
type: Number,
default: 0
}
},
data() {
return {
page: 1,
perPage: 25,
initialPerPage: 25,
paginationConfig: {
hideCount: true,
chunksNavigation: "scroll",
},
importantHeartBeatListLength: 0,
displayedRecords: [],
dockerRunCommand: "",
2023-12-26 04:12:44 +08:00
showAgentForm: false,
showRemoveAgentDialog: {},
showEditAgentNameDialog: {},
2023-12-26 04:12:44 +08:00
connectingAgent: false,
agent: {
url: "http://",
username: "",
password: "",
name: "",
updatedName: "",
2023-12-26 04:12:44 +08:00
}
2023-11-11 22:18:37 +08:00
};
},
computed: {
activeNum() {
return this.getStatusNum("active");
},
inactiveNum() {
return this.getStatusNum("inactive");
},
exitedNum() {
return this.getStatusNum("exited");
},
},
watch: {
perPage() {
this.$nextTick(() => {
this.getImportantHeartbeatListPaged();
});
},
page() {
this.getImportantHeartbeatListPaged();
},
},
mounted() {
this.initialPerPage = this.perPage;
window.addEventListener("resize", this.updatePerPage);
this.updatePerPage();
},
beforeUnmount() {
window.removeEventListener("resize", this.updatePerPage);
},
methods: {
2023-12-26 04:12:44 +08:00
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];
}
});
},
updateName(url, updatedName) {
this.$root.getSocket().emit("updateAgent", url, updatedName, (res) => {
this.$root.toastRes(res);
if (res.ok) {
this.showAgentForm = false;
this.agent = {
updatedName: "",
};
}
});
},
2023-11-11 22:18:37 +08:00
getStatusNum(statusName) {
let num = 0;
2023-12-26 04:12:44 +08:00
for (let stackName in this.$root.completeStackList) {
const stack = this.$root.completeStackList[stackName];
2023-11-11 22:18:37 +08:00
if (statusNameShort(stack.status) === statusName) {
num += 1;
}
}
return num;
},
convertDockerRun() {
if (this.dockerRunCommand.trim() === "docker run") {
throw new Error("Please enter a docker run command");
}
// composerize is working in dev, but after "vite build", it is not working
// So pass to backend to do the conversion
this.$root.getSocket().emit("composerize", this.dockerRunCommand, (res) => {
if (res.ok) {
this.$root.composeTemplate = res.composeTemplate;
this.$router.push("/compose");
} else {
this.$root.toastRes(res);
}
});
},
/**
* Updates the displayed records when a new important heartbeat arrives.
* @param {object} heartbeat - The heartbeat object received.
* @returns {void}
*/
onNewImportantHeartbeat(heartbeat) {
if (this.page === 1) {
this.displayedRecords.unshift(heartbeat);
if (this.displayedRecords.length > this.perPage) {
this.displayedRecords.pop();
}
this.importantHeartBeatListLength += 1;
}
},
/**
* Retrieves the length of the important heartbeat list for all monitors.
* @returns {void}
*/
getImportantHeartbeatListLength() {
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", null, (res) => {
if (res.ok) {
this.importantHeartBeatListLength = res.count;
this.getImportantHeartbeatListPaged();
}
});
},
/**
* Retrieves the important heartbeat list for the current page.
* @returns {void}
*/
getImportantHeartbeatListPaged() {
const offset = (this.page - 1) * this.perPage;
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", null, offset, this.perPage, (res) => {
if (res.ok) {
this.displayedRecords = res.data;
}
});
},
/**
* Updates the number of items shown per page based on the available height.
* @returns {void}
*/
updatePerPage() {
const tableContainer = this.$refs.tableContainer;
const tableContainerHeight = tableContainer.offsetHeight;
const availableHeight = window.innerHeight - tableContainerHeight;
const additionalPerPage = Math.floor(availableHeight / 58);
if (additionalPerPage > 0) {
this.perPage = Math.max(this.initialPerPage, this.perPage + additionalPerPage);
} else {
this.perPage = this.initialPerPage;
}
},
}
2023-11-11 22:18:37 +08:00
};
</script>
<style lang="scss" scoped>
@import "../styles/vars";
.num {
font-size: 30px;
font-weight: bold;
display: block;
&.active {
color: $primary;
}
&.exited {
color: $danger;
}
}
.shadow-box {
padding: 20px;
}
table {
font-size: 14px;
tr {
transition: all ease-in-out 0.2ms;
}
@media (max-width: 550px) {
table-layout: fixed;
overflow-wrap: break-word;
}
}
.docker-run {
background-color: $dark-bg !important;
border: none;
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
2023-11-11 22:18:37 +08:00
}
2023-12-26 04:12:44 +08:00
.first-row .shadow-box {
}
.remove-agent {
cursor: pointer;
color: rgba(255, 255, 255, 0.3);
}
.agent {
a {
text-decoration: none;
}
}
2023-11-11 22:18:37 +08:00
</style>