mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-23 14:29:42 -05:00
WIP: Add tags functionality
WIP: add color column, show tags WIP: Improve TagsManager styling & workflow WIP: Improve styling & validation, use translation WIP: Complete TagsManager functionality WIP: Add tags display in monitorList & Details Fix: update tags list after edit Fix: slightly improve tags styling Fix: Improve mobile UI Fix: Fix tags not showing on create monitor Fix: bring existingTags inside tagsManager Fix: remove unused tags prop Fix: Fix formatting, bump db version
This commit is contained in:
parent
50175b733c
commit
6e3a904aaa
19
db/patch10.sql
Normal file
19
db/patch10.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
CREATE TABLE tag (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
color VARCHAR(255) NOT NULL,
|
||||||
|
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE monitor_tag (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
monitor_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
value TEXT,
|
||||||
|
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id);
|
||||||
|
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id);
|
@ -37,7 +37,7 @@ class Database {
|
|||||||
* The finally version should be 10 after merged tag feature
|
* The finally version should be 10 after merged tag feature
|
||||||
* @deprecated Use patchList for any new feature
|
* @deprecated Use patchList for any new feature
|
||||||
*/
|
*/
|
||||||
static latestVersion = 9;
|
static latestVersion = 10;
|
||||||
|
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
|
|
||||||
|
@ -32,6 +32,8 @@ class Monitor extends BeanModel {
|
|||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@ -52,6 +54,7 @@ class Monitor extends BeanModel {
|
|||||||
dns_resolve_server: this.dns_resolve_server,
|
dns_resolve_server: this.dns_resolve_server,
|
||||||
dns_last_result: this.dns_last_result,
|
dns_last_result: this.dns_last_result,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
|
tags: tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
13
server/model/tag.js
Normal file
13
server/model/tag.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Tag extends BeanModel {
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this._id,
|
||||||
|
name: this._name,
|
||||||
|
color: this._color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Tag;
|
169
server/server.js
169
server/server.js
@ -514,6 +514,22 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("getMonitorList", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
await sendMonitorList(socket);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("getMonitor", async (monitorID, callback) => {
|
socket.on("getMonitor", async (monitorID, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
@ -608,6 +624,159 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("getTags", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
const list = await R.findAll("tag")
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
tags: list.map(bean => bean.toJSON()),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("addTag", async (tag, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
let bean = R.dispense("tag")
|
||||||
|
bean.name = tag.name
|
||||||
|
bean.color = tag.color
|
||||||
|
await R.store(bean)
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
tag: await bean.toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("editTag", async (tag, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
let bean = await R.findOne("monitor", " id = ? ", [ tag.id ])
|
||||||
|
bean.name = tag.name
|
||||||
|
bean.color = tag.color
|
||||||
|
await R.store(bean)
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
tag: await bean.toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteTag", async (tagID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ])
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("addMonitorTag", async (tagID, monitorID, value, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [
|
||||||
|
tagID,
|
||||||
|
monitorID,
|
||||||
|
value,
|
||||||
|
])
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("editMonitorTag", async (tagID, monitorID, value, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
await R.exec("UPDATE monitor_tag SET value = ? WHERE tag_id = ? AND monitor_id = ?", [
|
||||||
|
value,
|
||||||
|
tagID,
|
||||||
|
monitorID,
|
||||||
|
])
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Edited Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteMonitorTag", async (tagID, monitorID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM monitor_tag WHERE tag_id = ? AND monitor_id = ?", [
|
||||||
|
tagID,
|
||||||
|
monitorID,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Cleanup unused Tags
|
||||||
|
await R.exec("delete from tag where ( select count(*) from monitor_tag mt where tag.id = mt.tag_id ) = 0");
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("changePassword", async (password, callback) => {
|
socket.on("changePassword", async (password, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
<Uptime :monitor="item" type="24" :pill="true" />
|
<Uptime :monitor="item" type="24" :pill="true" />
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tags">
|
||||||
|
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||||
@ -29,10 +32,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||||
import Uptime from "../components/Uptime.vue";
|
import Uptime from "../components/Uptime.vue";
|
||||||
|
import Tag from "../components/Tag.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Uptime,
|
Uptime,
|
||||||
HeartbeatBar,
|
HeartbeatBar,
|
||||||
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
@ -140,4 +146,11 @@ export default {
|
|||||||
.monitorItem {
|
.monitorItem {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
padding-left: 62px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
68
src/components/Tag.vue
Normal file
68
src/components/Tag.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tag-wrapper rounded d-inline-flex"
|
||||||
|
:class="{ 'px-3': size == 'normal',
|
||||||
|
'py-1': size == 'normal',
|
||||||
|
'm-2': size == 'normal',
|
||||||
|
'px-2': size == 'sm',
|
||||||
|
'py-0': size == 'sm',
|
||||||
|
'm-1': size == 'sm',
|
||||||
|
}"
|
||||||
|
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||||
|
>
|
||||||
|
<span class="tag-text">{{ displayText }}</span>
|
||||||
|
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
|
||||||
|
<font-awesome-icon icon="times" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "normal",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayText() {
|
||||||
|
if (this.item.value == "") {
|
||||||
|
return this.item.name;
|
||||||
|
} else {
|
||||||
|
return `${this.item.name}: ${this.item.value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tag-wrapper {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-text {
|
||||||
|
padding-bottom: 1px !important;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove {
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 24px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
313
src/components/TagsManager.vue
Normal file
313
src/components/TagsManager.vue
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-3">{{ $t("Tags") }}</h4>
|
||||||
|
<div class="mb-3 p-1">
|
||||||
|
<tag
|
||||||
|
v-for="item in selectedTags"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
:remove="deleteTag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<vue-multiselect
|
||||||
|
v-model="newDraftTag.select"
|
||||||
|
class="mb-2"
|
||||||
|
:options="tagOptions"
|
||||||
|
:multiple="false"
|
||||||
|
:searchable="true"
|
||||||
|
:placeholder="$t('Add New below or Select...')"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #singleLabel="{ option }">
|
||||||
|
<div class="py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</vue-multiselect>
|
||||||
|
<div v-if="newDraftTag.select?.id == null" class="d-flex mb-2">
|
||||||
|
<div class="w-50 pe-2">
|
||||||
|
<input v-model="newDraftTag.name" class="form-control" :class="{'is-invalid': newDraftTag.nameInvalid}" placeholder="name" />
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $t("Tag with this name already exist.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-50 ps-2">
|
||||||
|
<vue-multiselect
|
||||||
|
v-model="newDraftTag.color"
|
||||||
|
:options="colorOptions"
|
||||||
|
:multiple="false"
|
||||||
|
:searchable="true"
|
||||||
|
:placeholder="$t('color')"
|
||||||
|
track-by="color"
|
||||||
|
label="name"
|
||||||
|
select-label=""
|
||||||
|
deselect-label=""
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #singleLabel="{ option }">
|
||||||
|
<div class="py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</vue-multiselect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input v-model="newDraftTag.value" class="form-control mb-2" :placeholder="$t('value (optional)')" />
|
||||||
|
<div class="mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary float-end"
|
||||||
|
:disabled="processing || newDraftTag.invalid"
|
||||||
|
@click.stop="addDraftTag"
|
||||||
|
>
|
||||||
|
{{ $t("Add") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueMultiselect from "vue-multiselect";
|
||||||
|
import Tag from "../components/Tag.vue";
|
||||||
|
import { useToast } from "vue-toastification"
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Tag,
|
||||||
|
VueMultiselect,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
preSelectedTags: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
existingTags: [],
|
||||||
|
processing: false,
|
||||||
|
newTags: [],
|
||||||
|
deleteTags: [],
|
||||||
|
newDraftTag: {
|
||||||
|
name: null,
|
||||||
|
select: null,
|
||||||
|
color: null,
|
||||||
|
value: "",
|
||||||
|
invalid: true,
|
||||||
|
nameInvalid: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tagOptions() {
|
||||||
|
return this.existingTags;
|
||||||
|
},
|
||||||
|
selectedTags() {
|
||||||
|
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
|
||||||
|
},
|
||||||
|
colorOptions() {
|
||||||
|
return [
|
||||||
|
{ name: this.$t("Gray"),
|
||||||
|
color: "#4B5563" },
|
||||||
|
{ name: this.$t("Red"),
|
||||||
|
color: "#DC2626" },
|
||||||
|
{ name: this.$t("Orange"),
|
||||||
|
color: "#D97706" },
|
||||||
|
{ name: this.$t("Green"),
|
||||||
|
color: "#059669" },
|
||||||
|
{ name: this.$t("Blue"),
|
||||||
|
color: "#2563EB" },
|
||||||
|
{ name: this.$t("Indigo"),
|
||||||
|
color: "#4F46E5" },
|
||||||
|
{ name: this.$t("Purple"),
|
||||||
|
color: "#7C3AED" },
|
||||||
|
{ name: this.$t("Pink"),
|
||||||
|
color: "#DB2777" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"newDraftTag.select": function (newSelected) {
|
||||||
|
this.newDraftTag.select = newSelected;
|
||||||
|
this.validateDraftTag();
|
||||||
|
},
|
||||||
|
"newDraftTag.name": function (newName) {
|
||||||
|
this.newDraftTag.name = newName.trim();
|
||||||
|
this.validateDraftTag();
|
||||||
|
},
|
||||||
|
"newDraftTag.color": function (newColor) {
|
||||||
|
this.newDraftTag.color = newColor;
|
||||||
|
this.validateDraftTag();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getExistingTags();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getExistingTags() {
|
||||||
|
this.$root.getSocket().emit("getTags", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.existingTags = res.tags;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteTag(item) {
|
||||||
|
if (item.new) {
|
||||||
|
// Undo Adding a new Tag
|
||||||
|
this.newTags = this.newTags.filter(tag => tag.name != item.name && tag.value != item.value);
|
||||||
|
} else {
|
||||||
|
// Remove an Existing Tag
|
||||||
|
this.deleteTags.push(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validateDraftTag() {
|
||||||
|
if (this.newDraftTag.select != null) {
|
||||||
|
// Select an existing tag, no need to validate
|
||||||
|
this.newDraftTag.invalid = false;
|
||||||
|
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
|
||||||
|
// Try to create new tag with existing name
|
||||||
|
this.newDraftTag.nameInvalid = true;
|
||||||
|
this.newDraftTag.invalid = true;
|
||||||
|
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
|
||||||
|
// Missing form inputs
|
||||||
|
this.newDraftTag.nameInvalid = false;
|
||||||
|
this.newDraftTag.invalid = true;
|
||||||
|
} else {
|
||||||
|
// Looks valid
|
||||||
|
this.newDraftTag.invalid = false;
|
||||||
|
this.newDraftTag.nameInvalid = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textColor(option) {
|
||||||
|
if (option.color) {
|
||||||
|
return "white";
|
||||||
|
} else {
|
||||||
|
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addDraftTag() {
|
||||||
|
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||||
|
if (this.newDraftTag.select != null) {
|
||||||
|
// Add an existing Tag
|
||||||
|
this.newTags.push({
|
||||||
|
id: this.newDraftTag.select.id,
|
||||||
|
color: this.newDraftTag.select.color,
|
||||||
|
name: this.newDraftTag.select.name,
|
||||||
|
value: this.newDraftTag.value,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Add new Tag
|
||||||
|
this.newTags.push({
|
||||||
|
color: this.newDraftTag.color.color,
|
||||||
|
name: this.newDraftTag.name,
|
||||||
|
value: this.newDraftTag.value,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addTagAsync(newTag) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("addTag", newTag, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addMonitorTagAsync(tagId, monitorId, value) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteMonitorTagAsync(tagId, monitorId) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async submit(monitorId) {
|
||||||
|
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
for (const newTag of this.newTags) {
|
||||||
|
let tagId;
|
||||||
|
if (newTag.id == null) {
|
||||||
|
let newTagResult;
|
||||||
|
await this.addTagAsync(newTag).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
newTagResult = false;
|
||||||
|
}
|
||||||
|
newTagResult = res.tag;
|
||||||
|
});
|
||||||
|
if (!newTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tagId = newTagResult.id;
|
||||||
|
} else {
|
||||||
|
tagId = newTag.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newMonitorTagResult;
|
||||||
|
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
newMonitorTagResult = false;
|
||||||
|
}
|
||||||
|
newMonitorTagResult = true;
|
||||||
|
});
|
||||||
|
if (!newMonitorTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deleteTag of this.deleteTags) {
|
||||||
|
let deleteMonitorTagResult;
|
||||||
|
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
deleteMonitorTagResult = false;
|
||||||
|
}
|
||||||
|
deleteMonitorTagResult = true;
|
||||||
|
});
|
||||||
|
if (!deleteMonitorTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getExistingTags();
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
37
src/icon.js
37
src/icon.js
@ -1,10 +1,37 @@
|
|||||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"
|
import {
|
||||||
|
faArrowAltCircleUp,
|
||||||
|
faCog,
|
||||||
|
faEdit,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faList,
|
||||||
|
faPause,
|
||||||
|
faPlay,
|
||||||
|
faPlus,
|
||||||
|
faTachometerAlt,
|
||||||
|
faTimes,
|
||||||
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
// Add Free Font Awesome Icons here
|
// Add Free Font Awesome Icons here
|
||||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||||
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash);
|
library.add(
|
||||||
|
faArrowAltCircleUp,
|
||||||
|
faCog,
|
||||||
|
faEdit,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faList,
|
||||||
|
faPause,
|
||||||
|
faPlay,
|
||||||
|
faPlus,
|
||||||
|
faTachometerAlt,
|
||||||
|
faTimes,
|
||||||
|
faTrash,
|
||||||
|
);
|
||||||
|
|
||||||
|
export { FontAwesomeIcon };
|
||||||
|
|
||||||
export { FontAwesomeIcon }
|
|
||||||
|
@ -266,6 +266,10 @@ export default {
|
|||||||
socket.emit("twoFAStatus", callback)
|
socket.emit("twoFAStatus", callback)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getMonitorList(callback) {
|
||||||
|
socket.emit("getMonitorList", callback)
|
||||||
|
},
|
||||||
|
|
||||||
add(monitor, callback) {
|
add(monitor, callback) {
|
||||||
socket.emit("add", monitor, callback)
|
socket.emit("add", monitor, callback)
|
||||||
},
|
},
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
<transition name="slide-fade" appear>
|
<transition name="slide-fade" appear>
|
||||||
<div v-if="monitor">
|
<div v-if="monitor">
|
||||||
<h1> {{ monitor.name }}</h1>
|
<h1> {{ monitor.name }}</h1>
|
||||||
|
<div class="tags">
|
||||||
|
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
<p class="url">
|
<p class="url">
|
||||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
|
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
|
||||||
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
|
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||||
@ -213,6 +216,7 @@ import CountUp from "../components/CountUp.vue";
|
|||||||
import Uptime from "../components/Uptime.vue";
|
import Uptime from "../components/Uptime.vue";
|
||||||
import Pagination from "v-pagination-3";
|
import Pagination from "v-pagination-3";
|
||||||
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
|
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
|
||||||
|
import Tag from "../components/Tag.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -224,6 +228,7 @@ export default {
|
|||||||
Status,
|
Status,
|
||||||
Pagination,
|
Pagination,
|
||||||
PingChart,
|
PingChart,
|
||||||
|
Tag,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -503,4 +508,12 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags > div:first-child {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -158,6 +158,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 mb-1">
|
<div class="mt-5 mb-1">
|
||||||
<button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
<button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||||
</div>
|
</div>
|
||||||
@ -197,6 +201,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||||
|
import TagsManager from "../components/TagsManager.vue";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification"
|
||||||
import VueMultiselect from "vue-multiselect"
|
import VueMultiselect from "vue-multiselect"
|
||||||
import { isDev } from "../util.ts";
|
import { isDev } from "../util.ts";
|
||||||
@ -205,6 +210,7 @@ const toast = useToast()
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationDialog,
|
NotificationDialog,
|
||||||
|
TagsManager,
|
||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -317,22 +323,28 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
submit() {
|
async submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
if (this.isAdd) {
|
if (this.isAdd) {
|
||||||
this.$root.add(this.monitor, (res) => {
|
this.$root.add(this.monitor, async (res) => {
|
||||||
this.processing = false;
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
await this.$refs.tagsManager.submit(res.monitorID);
|
||||||
|
|
||||||
toast.success(res.msg);
|
toast.success(res.msg);
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.getMonitorList();
|
||||||
this.$router.push("/dashboard/" + res.monitorID)
|
this.$router.push("/dashboard/" + res.monitorID)
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
|
this.processing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
await this.$refs.tagsManager.submit(this.monitor.id);
|
||||||
|
|
||||||
this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
|
this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res)
|
||||||
@ -357,6 +369,8 @@ export default {
|
|||||||
.multiselect__tags {
|
.multiselect__tags {
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid #ced4da;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 6px 40px 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect--active .multiselect__tags {
|
.multiselect--active .multiselect__tags {
|
||||||
@ -373,9 +387,25 @@ export default {
|
|||||||
|
|
||||||
.multiselect__tag {
|
.multiselect__tag {
|
||||||
border-radius: 50rem;
|
border-radius: 50rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 6px 26px 6px 10px;
|
||||||
background: $primary !important;
|
background: $primary !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiselect__placeholder {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
opacity: 0.67;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input, .multiselect__single {
|
||||||
|
line-height: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
.multiselect__tag {
|
.multiselect__tag {
|
||||||
color: $dark-font-color2;
|
color: $dark-font-color2;
|
||||||
|
Loading…
Reference in New Issue
Block a user