diff --git a/db/patch10.sql b/db/patch10.sql new file mode 100644 index 00000000..488db116 --- /dev/null +++ b/db/patch10.sql @@ -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); diff --git a/server/database.js b/server/database.js index 1ee2026d..31cb795e 100644 --- a/server/database.js +++ b/server/database.js @@ -39,7 +39,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; diff --git a/server/model/monitor.js b/server/model/monitor.js index 2e0c0387..3b445078 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -45,6 +45,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, @@ -65,6 +67,7 @@ class Monitor extends BeanModel { dns_resolve_server: this.dns_resolve_server, dns_last_result: this.dns_last_result, notificationIDList, + tags: tags, }; } diff --git a/server/model/tag.js b/server/model/tag.js new file mode 100644 index 00000000..748280a7 --- /dev/null +++ b/server/model/tag.js @@ -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; diff --git a/server/server.js b/server/server.js index 71e52ca1..9d8a57ee 100644 --- a/server/server.js +++ b/server/server.js @@ -524,6 +524,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) @@ -618,6 +634,160 @@ 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, value, callback) => { + try { + checkLogin(socket) + + await R.exec("DELETE FROM monitor_tag WHERE tag_id = ? AND monitor_id = ? AND value = ?", [ + tagID, + monitorID, + value, + ]) + + // 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) diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue index 5dae04d0..fb3fcfb0 100644 --- a/src/components/MonitorList.vue +++ b/src/components/MonitorList.vue @@ -1,44 +1,69 @@ @@ -87,7 +126,51 @@ export default { padding-right: 5px !important; } +.list-header { + border-bottom: 1px solid #dee2e6; + border-radius: 10px 10px 0 0; + margin: -10px; + margin-bottom: 10px; + padding: 10px; + display: flex; + justify-content: space-between; + + .dark & { + background-color: #161b22; + border-bottom: 0; + } +} + +@media (max-width: 770px) { + .list-header { + margin: -20px; + margin-bottom: 10px; + padding: 5px; + } +} + +.search-wrapper { + display: flex; + align-items: center; +} + +.search-icon { + padding: 10px; + color: #c0c0c0; +} + +.search-input { + max-width: 15em; +} + .monitorItem { width: 100%; } + +.tags { + padding-left: 62px; + display: flex; + flex-wrap: wrap; + gap: 0; +} diff --git a/src/components/Tag.vue b/src/components/Tag.vue new file mode 100644 index 00000000..434358aa --- /dev/null +++ b/src/components/Tag.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/src/components/TagsManager.vue b/src/components/TagsManager.vue new file mode 100644 index 00000000..82025031 --- /dev/null +++ b/src/components/TagsManager.vue @@ -0,0 +1,405 @@ + + + + + diff --git a/src/icon.js b/src/icon.js index 4ce5b08b..8175e09d 100644 --- a/src/icon.js +++ b/src/icon.js @@ -1,10 +1,41 @@ -import { library } from "@fortawesome/fontawesome-svg-core" -import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash, faCheckCircle, faStream, faSave, faExclamationCircle } from "@fortawesome/free-solid-svg-icons" +import { library } from "@fortawesome/fontawesome-svg-core"; +import { + faArrowAltCircleUp, + faCog, + faEdit, + faEye, + faEyeSlash, + faList, + faPause, + faPlay, + faPlus, + faSearch, + faTachometerAlt, + faTimes, + faTrash, + faCheckCircle, faStream, faSave, faExclamationCircle +} 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, faCheckCircle, faStream, faSave, faExclamationCircle); +library.add( + faArrowAltCircleUp, + faCog, + faEdit, + faEye, + faEyeSlash, + faList, + faPause, + faPlay, + faPlus, + faSearch, + faTachometerAlt, + faTimes, + faTrash, + faCheckCircle, faStream, faSave, faExclamationCircle +); + +export { FontAwesomeIcon }; -export { FontAwesomeIcon } diff --git a/src/mixins/socket.js b/src/mixins/socket.js index eaf13db4..715b4ef1 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -287,6 +287,10 @@ export default { socket.emit("twoFAStatus", callback) }, + getMonitorList(callback) { + socket.emit("getMonitorList", callback) + }, + add(monitor, callback) { socket.emit("add", monitor, callback) }, diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 9092b179..c992d240 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -2,6 +2,9 @@

{{ monitor.name }}

+
+ +

{{ monitor.url }} TCP Ping {{ monitor.hostname }}:{{ monitor.port }} @@ -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; +} + diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index f98bb756..cc836efe 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -158,6 +158,10 @@

+
+ +
+
@@ -197,6 +201,7 @@