diff options
Diffstat (limited to 'src/components/TagEditDialog.vue')
-rw-r--r-- | src/components/TagEditDialog.vue | 485 |
1 files changed, 485 insertions, 0 deletions
diff --git a/src/components/TagEditDialog.vue b/src/components/TagEditDialog.vue new file mode 100644 index 0000000..77fce26 --- /dev/null +++ b/src/components/TagEditDialog.vue @@ -0,0 +1,485 @@ +<template> + <form @submit.prevent="submit"> + <div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 id="exampleModalLabel" class="modal-title"> + {{ $t("Edit Tag") }} + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" /> + </div> + <div class="modal-body"> + <div class="mb-3"> + <label for="tag-name" class="form-label">{{ $t("Name") }}</label> + <input + id="tag-name" + v-model="tag.name" + type="text" + class="form-control" + :class="{'is-invalid': nameInvalid}" + required + > + <div class="invalid-feedback"> + {{ $t("Tag with this name already exist.") }} + </div> + </div> + + <div class="mb-3"> + <label for="tag-color" class="form-label">{{ $t("color") }}</label> + <div class="d-flex"> + <div class="col-8 pe-1"> + <vue-multiselect + v-model="selectedColor" + :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 class="col-4 ps-1"> + <input id="tag-color-hex" v-model="tag.color" type="text" class="form-control"> + </div> + </div> + </div> + + <div class="mb-3"> + <label for="tag-monitors" class="form-label">{{ $tc("Monitor", selectedMonitors.length) }}</label> + <div class="tag-monitors-list"> + <router-link v-for="monitor in selectedMonitors" :key="monitor.id" class="d-flex align-items-center justify-content-between text-decoration-none tag-monitors-list-row py-2 px-3" :to="monitorURL(monitor.id)" @click="modal.hide()"> + <span>{{ monitor.name }}</span> + <button type="button" class="btn-rm-monitor btn btn-outline-danger ms-2 py-1" @click.stop.prevent="removeMonitor(monitor.id)"> + <font-awesome-icon class="" icon="times" /> + </button> + </router-link> + </div> + <div v-if="allMonitorList.length > 0" class="pt-3"> + <label class="form-label">{{ $t("Add a monitor") }}:</label> + <VueMultiselect + v-model="selectedAddMonitor" + :options="allMonitorList" + :multiple="false" + :searchable="true" + :placeholder="$t('Add a monitor')" + label="name" + trackBy="name" + class="mt-1" + > + <template #option="{ option }"> + <div class="d-inline-flex"> + <span>{{ option.name }} <Tag v-for="monitorTag in option.tags" :key="monitorTag" :item="monitorTag" :size="'sm'" /></span> + </div> + </template> + </VueMultiselect> + </div> + </div> + </div> + + <div class="modal-footer"> + <button v-if="tag && tag.id !== null" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm"> + {{ $t("Delete") }} + </button> + <button type="submit" class="btn btn-primary" :disabled="processing"> + <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div> + {{ $t("Save") }} + </button> + </div> + </div> + </div> + </div> + </form> + + <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteTag"> + {{ $t("confirmDeleteTagMsg") }} + </Confirm> +</template> + +<script> +import { Modal } from "bootstrap"; +import Confirm from "./Confirm.vue"; +import Tag from "./Tag.vue"; +import VueMultiselect from "vue-multiselect"; +import { colorOptions } from "../util-frontend"; +import { getMonitorRelativeURL } from "../util.ts"; + +export default { + components: { + VueMultiselect, + Confirm, + Tag, + }, + props: { + updated: { + type: Function, + default: () => {}, + }, + existingTags: { + type: Array, + default: () => [], + }, + }, + data() { + return { + modal: null, + processing: false, + selectedColor: { + name: null, + color: null, + }, + tag: { + id: null, + name: "", + color: "", + // Do not set default value here, please scroll to show() + }, + monitors: [], + removingMonitor: [], + addingMonitor: [], + selectedAddMonitor: null, + nameInvalid: false, + }; + }, + + computed: { + colorOptions() { + if (!colorOptions(this).find(option => option.color === this.tag.color)) { + return colorOptions(this).concat( + { + name: "custom", + color: this.tag.color + }); + } else { + return colorOptions(this); + } + }, + selectedMonitors() { + return this.monitors + .concat(Object.values(this.$root.monitorList).filter(monitor => this.addingMonitor.includes(monitor.id))) + .filter(monitor => !this.removingMonitor.includes(monitor.id)); + }, + allMonitorList() { + return Object.values(this.$root.monitorList).filter(monitor => !this.selectedMonitors.includes(monitor)); + }, + }, + + watch: { + // Set color option to "Custom" when a unknown color is entered + "tag.color"(to, from) { + if (to !== "" && colorOptions(this).find(x => x.color === to) == null) { + this.selectedColor.name = this.$t("Custom"); + this.selectedColor.color = to; + } + }, + "tag.name"(to, from) { + if (to != null) { + this.validate(); + } + }, + selectedColor(to, from) { + if (to != null) { + this.tag.color = to.color; + } + }, + /** + * Selected a monitor and add to the list. + * @param {object} monitor Monitor to add + * @returns {void} + */ + selectedAddMonitor(monitor) { + if (monitor) { + if (this.removingMonitor.includes(monitor.id)) { + this.removingMonitor = this.removingMonitor.filter(id => id !== monitor.id); + } else { + this.addingMonitor.push(monitor.id); + } + this.selectedAddMonitor = null; + } + }, + }, + + mounted() { + this.modal = new Modal(this.$refs.modal); + }, + + methods: { + /** + * Show confirmation for deleting a tag + * @returns {void} + */ + deleteConfirm() { + this.$refs.confirmDelete.show(); + }, + + /** + * Reset the editTag form + * @returns {void} + */ + reset() { + this.selectedColor = null; + this.tag = { + id: null, + name: "", + color: "", + }; + this.monitors = []; + this.removingMonitor = []; + this.addingMonitor = []; + }, + + /** + * Check for existing tags of the same name, set invalid input + * @returns {boolean} True if editing tag is valid + */ + validate() { + this.nameInvalid = false; + const sameName = this.existingTags.find((existingTag) => existingTag.name === this.tag.name); + if (sameName != null && sameName.id !== this.tag.id) { + this.nameInvalid = true; + return false; + } + return true; + }, + + /** + * Load tag information for display in the edit dialog + * @param {object} tag tag object to edit + * @returns {void} + */ + show(tag) { + if (tag) { + this.selectedColor = this.colorOptions.find(x => x.color === tag.color) ?? { + name: this.$t("Custom"), + color: tag.color + }; + this.tag.id = tag.id; + this.tag.name = tag.name; + this.tag.color = tag.color; + this.monitors = this.monitorsByTag(tag.id); + this.removingMonitor = []; + this.addingMonitor = []; + this.selectedAddMonitor = null; + } + + this.modal.show(); + }, + + /** + * Submit tag and monitorTag changes to server + * @returns {Promise<void>} + */ + async submit() { + this.processing = true; + let editResult = true; + + if (!this.validate()) { + this.processing = false; + return; + } + + if (this.tag.id == null) { + await this.addTagAsync(this.tag).then((res) => { + if (!res.ok) { + this.$root.toastRes(res.msg); + editResult = false; + } else { + this.tag.id = res.tag.id; + this.updated(); + } + }); + } + + if (!editResult) { + return; + } + + for (let addId of this.addingMonitor) { + await this.addMonitorTagAsync(this.tag.id, addId, "").then((res) => { + if (!res.ok) { + this.$root.toastError(res.msg); + editResult = false; + } + }); + } + + for (let removeId of this.removingMonitor) { + this.monitors.find(monitor => monitor.id === removeId)?.tags.forEach(async (monitorTag) => { + await this.deleteMonitorTagAsync(this.tag.id, removeId, monitorTag.value).then((res) => { + if (!res.ok) { + this.$root.toastError(res.msg); + editResult = false; + } + }); + }); + } + + this.$root.getSocket().emit("editTag", this.tag, (res) => { + this.$root.toastRes(res); + this.processing = false; + + if (res.ok && editResult) { + this.updated(); + this.modal.hide(); + } + }); + }, + + /** + * Delete the editing tag from server + * @returns {Promise<void>} + */ + async deleteTag() { + this.processing = true; + await this.deleteTagAsync(this.tag.id).then((res) => { + this.$root.toastRes(res); + this.processing = false; + + if (res.ok) { + this.updated(); + this.modal.hide(); + } + }); + }, + + /** + * Remove a monitor from the monitors list locally + * @param {number} id id of the tag to remove + * @returns {void} + */ + removeMonitor(id) { + if (this.addingMonitor.includes(id)) { + this.addingMonitor = this.addingMonitor.filter(x => x !== id); + } else { + this.removingMonitor.push(id); + } + }, + + /** + * Get monitors which has a specific tag locally + * @param {number} tagId id of the tag to filter + * @returns {object[]} list of monitors which has a specific tag + */ + monitorsByTag(tagId) { + return Object.values(this.$root.monitorList).filter((monitor) => { + return monitor.tags.find(monitorTag => monitorTag.tag_id === tagId); + }); + }, + + /** + * Get URL of monitor + * @param {number} id ID of monitor + * @returns {string} Relative URL of monitor + */ + monitorURL(id) { + return getMonitorRelativeURL(id); + }, + + /** + * Add a tag asynchronously + * @param {object} newTag Object representing new tag to add + * @returns {Promise<void>} + */ + addTagAsync(newTag) { + return new Promise((resolve) => { + this.$root.getSocket().emit("addTag", newTag, resolve); + }); + }, + + /** + * Delete a tag asynchronously + * @param {number} tagId ID of tag to delete + * @returns {Promise<void>} + */ + deleteTagAsync(tagId) { + return new Promise((resolve) => { + this.$root.getSocket().emit("deleteTag", tagId, resolve); + }); + }, + + /** + * Add a tag to a monitor asynchronously + * @param {number} tagId ID of tag to add + * @param {number} monitorId ID of monitor to add tag to + * @param {string} value Value of tag + * @returns {Promise<void>} + */ + addMonitorTagAsync(tagId, monitorId, value) { + return new Promise((resolve) => { + this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve); + }); + }, + /** + * Delete a tag from a monitor asynchronously + * @param {number} tagId ID of tag to remove + * @param {number} monitorId ID of monitor to remove tag from + * @param {string} value Value of tag + * @returns {Promise<void>} + */ + deleteMonitorTagAsync(tagId, monitorId, value) { + return new Promise((resolve) => { + this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve); + }); + }, + }, +}; +</script> + +<style lang="scss" scoped> +@import "../assets/vars.scss"; + +.dark { + .modal-dialog .form-text, .modal-dialog p { + color: $dark-font-color; + } +} + +.btn-rm-monitor { + padding-left: 11px; + padding-right: 11px; +} + +.tag-monitors-list { + max-height: 40vh; + overflow-y: scroll; +} + +.tag-monitors-list .tag-monitors-list-row { + cursor: pointer; + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + + .dark & { + border-bottom: 1px solid $dark-border-color; + } + + &:hover { + background-color: $highlight-white; + } + + .dark &:hover { + background-color: $dark-bg2; + } +} + +</style> |