mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08: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:
		
							
								
								
									
										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
 | 
			
		||||
     * @deprecated Use patchList for any new feature
 | 
			
		||||
     */
 | 
			
		||||
    static latestVersion = 9;
 | 
			
		||||
    static latestVersion = 10;
 | 
			
		||||
 | 
			
		||||
    static noReject = true;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,8 @@ class Monitor extends BeanModel {
 | 
			
		||||
            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 {
 | 
			
		||||
            id: this.id,
 | 
			
		||||
            name: this.name,
 | 
			
		||||
@@ -52,6 +54,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
            dns_resolve_server: this.dns_resolve_server,
 | 
			
		||||
            dns_last_result: this.dns_last_result,
 | 
			
		||||
            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) => {
 | 
			
		||||
            try {
 | 
			
		||||
                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) => {
 | 
			
		||||
            try {
 | 
			
		||||
                checkLogin(socket)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,9 @@
 | 
			
		||||
                        <Uptime :monitor="item" type="24" :pill="true" />
 | 
			
		||||
                        {{ item.name }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="tags">
 | 
			
		||||
                        <Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
 | 
			
		||||
                    <HeartbeatBar size="small" :monitor-id="item.id" />
 | 
			
		||||
@@ -29,10 +32,13 @@
 | 
			
		||||
<script>
 | 
			
		||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
 | 
			
		||||
import Uptime from "../components/Uptime.vue";
 | 
			
		||||
import Tag from "../components/Tag.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        Uptime,
 | 
			
		||||
        HeartbeatBar,
 | 
			
		||||
        Tag,
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
        scrollbar: {
 | 
			
		||||
@@ -140,4 +146,11 @@ export default {
 | 
			
		||||
.monitorItem {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tags {
 | 
			
		||||
    padding-left: 62px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    gap: 0;
 | 
			
		||||
}
 | 
			
		||||
</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 { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"
 | 
			
		||||
import { library } from "@fortawesome/fontawesome-svg-core";
 | 
			
		||||
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 { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
 | 
			
		||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 | 
			
		||||
 | 
			
		||||
// Add Free Font Awesome Icons here
 | 
			
		||||
// 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)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        getMonitorList(callback) {
 | 
			
		||||
            socket.emit("getMonitorList", callback)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        add(monitor, callback) {
 | 
			
		||||
            socket.emit("add", monitor, callback)
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,9 @@
 | 
			
		||||
    <transition name="slide-fade" appear>
 | 
			
		||||
        <div v-if="monitor">
 | 
			
		||||
            <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">
 | 
			
		||||
                <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>
 | 
			
		||||
@@ -213,6 +216,7 @@ import CountUp from "../components/CountUp.vue";
 | 
			
		||||
import Uptime from "../components/Uptime.vue";
 | 
			
		||||
import Pagination from "v-pagination-3";
 | 
			
		||||
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
 | 
			
		||||
import Tag from "../components/Tag.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
@@ -224,6 +228,7 @@ export default {
 | 
			
		||||
        Status,
 | 
			
		||||
        Pagination,
 | 
			
		||||
        PingChart,
 | 
			
		||||
        Tag,
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
@@ -503,4 +508,12 @@ table {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tags {
 | 
			
		||||
    margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tags > div:first-child {
 | 
			
		||||
    margin-left: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -158,6 +158,10 @@
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </template>
 | 
			
		||||
 | 
			
		||||
                            <div class="my-3">
 | 
			
		||||
                                <tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <div class="mt-5 mb-1">
 | 
			
		||||
                                <button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
 | 
			
		||||
                            </div>
 | 
			
		||||
@@ -197,6 +201,7 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import NotificationDialog from "../components/NotificationDialog.vue";
 | 
			
		||||
import TagsManager from "../components/TagsManager.vue";
 | 
			
		||||
import { useToast } from "vue-toastification"
 | 
			
		||||
import VueMultiselect from "vue-multiselect"
 | 
			
		||||
import { isDev } from "../util.ts";
 | 
			
		||||
@@ -205,6 +210,7 @@ const toast = useToast()
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        NotificationDialog,
 | 
			
		||||
        TagsManager,
 | 
			
		||||
        VueMultiselect,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@@ -317,22 +323,28 @@ export default {
 | 
			
		||||
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        submit() {
 | 
			
		||||
        async submit() {
 | 
			
		||||
            this.processing = true;
 | 
			
		||||
 | 
			
		||||
            if (this.isAdd) {
 | 
			
		||||
                this.$root.add(this.monitor, (res) => {
 | 
			
		||||
                    this.processing = false;
 | 
			
		||||
                this.$root.add(this.monitor, async (res) => {
 | 
			
		||||
 | 
			
		||||
                    if (res.ok) {
 | 
			
		||||
                        await this.$refs.tagsManager.submit(res.monitorID);
 | 
			
		||||
 | 
			
		||||
                        toast.success(res.msg);
 | 
			
		||||
                        this.processing = false;
 | 
			
		||||
                        this.$root.getMonitorList();
 | 
			
		||||
                        this.$router.push("/dashboard/" + res.monitorID)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        toast.error(res.msg);
 | 
			
		||||
                        this.processing = false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
                await this.$refs.tagsManager.submit(this.monitor.id);
 | 
			
		||||
 | 
			
		||||
                this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
 | 
			
		||||
                    this.processing = false;
 | 
			
		||||
                    this.$root.toastRes(res)
 | 
			
		||||
@@ -357,6 +369,8 @@ export default {
 | 
			
		||||
    .multiselect__tags {
 | 
			
		||||
        border-radius: 1.5rem;
 | 
			
		||||
        border: 1px solid #ced4da;
 | 
			
		||||
        min-height: 38px;
 | 
			
		||||
        padding: 6px 40px 0 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .multiselect--active .multiselect__tags {
 | 
			
		||||
@@ -373,9 +387,25 @@ export default {
 | 
			
		||||
 | 
			
		||||
    .multiselect__tag {
 | 
			
		||||
        border-radius: 50rem;
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
        padding: 6px 26px 6px 10px;
 | 
			
		||||
        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 {
 | 
			
		||||
        .multiselect__tag {
 | 
			
		||||
            color: $dark-font-color2;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user