# Conflicts: # src/components/MonitorList.vue # src/icon.jspull/124/head
commit
9902c181bc
@ -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);
|
@ -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;
|
@ -0,0 +1,73 @@
|
|||||||
|
<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 lang="scss" scoped>
|
||||||
|
.tag-wrapper {
|
||||||
|
color: white;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
@ -0,0 +1,405 @@
|
|||||||
|
<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 class="p-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-add"
|
||||||
|
:disabled="processing"
|
||||||
|
@click.stop="showAddDialog"
|
||||||
|
>
|
||||||
|
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body">
|
||||||
|
<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?.name == null" class="d-flex mb-2">
|
||||||
|
<div class="w-50 pe-2">
|
||||||
|
<input v-model="newDraftTag.name" class="form-control"
|
||||||
|
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
||||||
|
:placeholder="$t('name')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
<div class="mb-2">
|
||||||
|
<input v-model="newDraftTag.value" class="form-control"
|
||||||
|
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
||||||
|
:placeholder="$t('value (optional)')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $t("Tag with this value already exist.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary float-end"
|
||||||
|
:disabled="processing || validateDraftTag.invalid"
|
||||||
|
@click.stop="addDraftTag"
|
||||||
|
>
|
||||||
|
{{ $t("Add") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
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 {
|
||||||
|
modal: null,
|
||||||
|
existingTags: [],
|
||||||
|
processing: false,
|
||||||
|
newTags: [],
|
||||||
|
deleteTags: [],
|
||||||
|
newDraftTag: {
|
||||||
|
name: null,
|
||||||
|
select: null,
|
||||||
|
color: null,
|
||||||
|
value: "",
|
||||||
|
invalid: true,
|
||||||
|
nameInvalid: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tagOptions() {
|
||||||
|
const tagOptions = this.existingTags;
|
||||||
|
for (const tag of this.newTags) {
|
||||||
|
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
|
||||||
|
tagOptions.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tagOptions;
|
||||||
|
},
|
||||||
|
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" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
validateDraftTag() {
|
||||||
|
let nameInvalid = false;
|
||||||
|
let valueInvalid = false;
|
||||||
|
let invalid = true;
|
||||||
|
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
|
||||||
|
// Undo removing a Tag
|
||||||
|
nameInvalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
invalid = false;
|
||||||
|
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
|
||||||
|
// Try to create new tag with existing name
|
||||||
|
nameInvalid = true;
|
||||||
|
invalid = true;
|
||||||
|
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
||||||
|
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
|
||||||
|
) || (
|
||||||
|
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
|
||||||
|
)).length > 0) {
|
||||||
|
// Try to add a tag with existing name and value
|
||||||
|
valueInvalid = true;
|
||||||
|
invalid = true;
|
||||||
|
} else if (this.newDraftTag.select != null) {
|
||||||
|
// Select an existing tag, no need to validate
|
||||||
|
invalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
|
||||||
|
// Missing form inputs
|
||||||
|
nameInvalid = false;
|
||||||
|
invalid = true;
|
||||||
|
} else {
|
||||||
|
// Looks valid
|
||||||
|
invalid = false;
|
||||||
|
nameInvalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
invalid,
|
||||||
|
nameInvalid,
|
||||||
|
valueInvalid,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
this.getExistingTags();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showAddDialog() {
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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) {
|
||||||
|
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
|
||||||
|
// Undo removing a tag
|
||||||
|
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
|
||||||
|
} else {
|
||||||
|
// 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.trim(),
|
||||||
|
value: this.newDraftTag.value,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.clearDraftTag();
|
||||||
|
},
|
||||||
|
clearDraftTag() {
|
||||||
|
this.newDraftTag = {
|
||||||
|
name: null,
|
||||||
|
select: null,
|
||||||
|
color: null,
|
||||||
|
value: "",
|
||||||
|
invalid: true,
|
||||||
|
nameInvalid: false,
|
||||||
|
};
|
||||||
|
this.modal.hide();
|
||||||
|
},
|
||||||
|
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, value) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onEnter() {
|
||||||
|
if (!this.validateDraftTag.invalid) {
|
||||||
|
this.addDraftTag();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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) {
|
||||||
|
// Create a New Tag
|
||||||
|
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;
|
||||||
|
// Assign the new ID to the tags of the same name & color
|
||||||
|
this.newTags.map(tag => {
|
||||||
|
if (tag.name == newTag.name && tag.color == newTag.color) {
|
||||||
|
tag.id = newTagResult.id;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tagId = newTag.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newMonitorTagResult;
|
||||||
|
// Assign tag to monitor
|
||||||
|
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, deleteTag.value).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
deleteMonitorTagResult = false;
|
||||||
|
}
|
||||||
|
deleteMonitorTagResult = true;
|
||||||
|
});
|
||||||
|
if (!deleteMonitorTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getExistingTags();
|
||||||
|
this.newTags = [];
|
||||||
|
this.deleteTags = [];
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn-add {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in new issue