Bert Verhelst
3 years ago
64 changed files with 10492 additions and 1034 deletions
@ -0,0 +1,7 @@ |
|||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
||||
|
BEGIN TRANSACTION; |
||||
|
|
||||
|
ALTER TABLE monitor |
||||
|
ADD retry_interval INTEGER default 0 not null; |
||||
|
|
||||
|
COMMIT; |
@ -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,21 @@ |
|||||
|
#!/usr/bin/env sh |
||||
|
|
||||
|
# set -e Exit the script if an error happens |
||||
|
set -e |
||||
|
PUID=${PUID=1000} |
||||
|
PGID=${PGID=1000} |
||||
|
|
||||
|
files_ownership () { |
||||
|
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link. |
||||
|
# -R Recursively descends the specified directories |
||||
|
# -c Like verbose but report only when a change is made |
||||
|
chown -hRc "$PUID":"$PGID" /app/data |
||||
|
} |
||||
|
|
||||
|
echo "==> Performing startup jobs and maintenance tasks" |
||||
|
files_ownership |
||||
|
|
||||
|
echo "==> Starting application with user $PUID group $PGID" |
||||
|
|
||||
|
# --clear-groups Clear supplementary groups. |
||||
|
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@" |
File diff suppressed because it is too large
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 9.5 KiB |
@ -0,0 +1,19 @@ |
|||||
|
{ |
||||
|
"name": "Uptime Kuma", |
||||
|
"short_name": "Uptime Kuma", |
||||
|
"start_url": "/", |
||||
|
"background_color": "#fff", |
||||
|
"display": "standalone", |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "icon-192x192.png", |
||||
|
"sizes": "192x192", |
||||
|
"type": "image/png" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "icon-512x512.png", |
||||
|
"sizes": "512x512", |
||||
|
"type": "image/png" |
||||
|
} |
||||
|
] |
||||
|
} |
@ -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,124 @@ |
|||||
|
const NotificationProvider = require("./notification-provider"); |
||||
|
const axios = require("axios"); |
||||
|
const { DOWN, UP } = require("../../src/util"); |
||||
|
|
||||
|
class Teams extends NotificationProvider { |
||||
|
name = "teams"; |
||||
|
|
||||
|
_statusMessageFactory = (status, monitorName) => { |
||||
|
if (status === DOWN) { |
||||
|
return `🔴 Application [${monitorName}] went down`; |
||||
|
} else if (status === UP) { |
||||
|
return `✅ Application [${monitorName}] is back online`; |
||||
|
} |
||||
|
return "Notification"; |
||||
|
}; |
||||
|
|
||||
|
_getThemeColor = (status) => { |
||||
|
if (status === DOWN) { |
||||
|
return "ff0000"; |
||||
|
} |
||||
|
if (status === UP) { |
||||
|
return "00e804"; |
||||
|
} |
||||
|
return "008cff"; |
||||
|
}; |
||||
|
|
||||
|
_notificationPayloadFactory = ({ |
||||
|
status, |
||||
|
monitorMessage, |
||||
|
monitorName, |
||||
|
monitorUrl, |
||||
|
}) => { |
||||
|
const notificationMessage = this._statusMessageFactory( |
||||
|
status, |
||||
|
monitorName |
||||
|
); |
||||
|
|
||||
|
const facts = []; |
||||
|
|
||||
|
if (monitorName) { |
||||
|
facts.push({ |
||||
|
name: "Monitor", |
||||
|
value: monitorName, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (monitorUrl) { |
||||
|
facts.push({ |
||||
|
name: "URL", |
||||
|
value: monitorUrl, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
"@context": "https://schema.org/extensions", |
||||
|
"@type": "MessageCard", |
||||
|
themeColor: this._getThemeColor(status), |
||||
|
summary: notificationMessage, |
||||
|
sections: [ |
||||
|
{ |
||||
|
activityImage: |
||||
|
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png", |
||||
|
activityTitle: "**Uptime Kuma**", |
||||
|
}, |
||||
|
{ |
||||
|
activityTitle: notificationMessage, |
||||
|
}, |
||||
|
{ |
||||
|
activityTitle: "**Description**", |
||||
|
text: monitorMessage, |
||||
|
facts, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
_sendNotification = async (webhookUrl, payload) => { |
||||
|
await axios.post(webhookUrl, payload); |
||||
|
}; |
||||
|
|
||||
|
_handleGeneralNotification = (webhookUrl, msg) => { |
||||
|
const payload = this._notificationPayloadFactory({ |
||||
|
monitorMessage: msg |
||||
|
}); |
||||
|
|
||||
|
return this._sendNotification(webhookUrl, payload); |
||||
|
}; |
||||
|
|
||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
||||
|
let okMsg = "Sent Successfully. "; |
||||
|
|
||||
|
try { |
||||
|
if (heartbeatJSON == null) { |
||||
|
await this._handleGeneralNotification(notification.webhookUrl, msg); |
||||
|
return okMsg; |
||||
|
} |
||||
|
|
||||
|
let url; |
||||
|
|
||||
|
if (monitorJSON["type"] === "port") { |
||||
|
url = monitorJSON["hostname"]; |
||||
|
if (monitorJSON["port"]) { |
||||
|
url += ":" + monitorJSON["port"]; |
||||
|
} |
||||
|
} else { |
||||
|
url = monitorJSON["url"]; |
||||
|
} |
||||
|
|
||||
|
const payload = this._notificationPayloadFactory({ |
||||
|
monitorMessage: heartbeatJSON.msg, |
||||
|
monitorName: monitorJSON.name, |
||||
|
monitorUrl: url, |
||||
|
status: heartbeatJSON.status, |
||||
|
}); |
||||
|
|
||||
|
await this._sendNotification(notification.webhookUrl, payload); |
||||
|
return okMsg; |
||||
|
} catch (error) { |
||||
|
this.throwGeneralAxiosError(error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = Teams; |
@ -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> |
@ -0,0 +1,29 @@ |
|||||
|
<template> |
||||
|
<div class="mb-3"> |
||||
|
<label for="teams-webhookurl" class="form-label">Webhook URL</label> |
||||
|
<input |
||||
|
id="teams-webhookurl" |
||||
|
v-model="$parent.notification.webhookUrl" |
||||
|
type="text" |
||||
|
class="form-control" |
||||
|
required |
||||
|
/> |
||||
|
<div class="form-text"> |
||||
|
You can learn how to create a webhook url |
||||
|
<a |
||||
|
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook" |
||||
|
target="_blank" |
||||
|
>here</a>. |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
data() { |
||||
|
return { |
||||
|
name: "teams", |
||||
|
}; |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
@ -0,0 +1,48 @@ |
|||||
|
import { createI18n } from "vue-i18n"; |
||||
|
import daDK from "./languages/da-DK"; |
||||
|
import deDE from "./languages/de-DE"; |
||||
|
import en from "./languages/en"; |
||||
|
import esEs from "./languages/es-ES"; |
||||
|
import etEE from "./languages/et-EE"; |
||||
|
import frFR from "./languages/fr-FR"; |
||||
|
import itIT from "./languages/it-IT"; |
||||
|
import ja from "./languages/ja"; |
||||
|
import koKR from "./languages/ko-KR"; |
||||
|
import nlNL from "./languages/nl-NL"; |
||||
|
import pl from "./languages/pl"; |
||||
|
import ruRU from "./languages/ru-RU"; |
||||
|
import sr from "./languages/sr"; |
||||
|
import srLatn from "./languages/sr-latn"; |
||||
|
import trTR from "./languages/tr-TR"; |
||||
|
import svSE from "./languages/sv-SE"; |
||||
|
import zhCN from "./languages/zh-CN"; |
||||
|
import zhHK from "./languages/zh-HK"; |
||||
|
|
||||
|
const languageList = { |
||||
|
en, |
||||
|
"zh-HK": zhHK, |
||||
|
"de-DE": deDE, |
||||
|
"nl-NL": nlNL, |
||||
|
"es-ES": esEs, |
||||
|
"fr-FR": frFR, |
||||
|
"it-IT": itIT, |
||||
|
"ja": ja, |
||||
|
"da-DK": daDK, |
||||
|
"sr": sr, |
||||
|
"sr-latn": srLatn, |
||||
|
"sv-SE": svSE, |
||||
|
"tr-TR": trTR, |
||||
|
"ko-KR": koKR, |
||||
|
"ru-RU": ruRU, |
||||
|
"zh-CN": zhCN, |
||||
|
"pl": pl, |
||||
|
"et-EE": etEE, |
||||
|
}; |
||||
|
|
||||
|
export const i18n = createI18n({ |
||||
|
locale: localStorage.locale || "en", |
||||
|
fallbackLocale: "en", |
||||
|
silentFallbackWarn: true, |
||||
|
silentTranslationWarn: false, |
||||
|
messages: languageList, |
||||
|
}); |
@ -0,0 +1,171 @@ |
|||||
|
export default { |
||||
|
languageName: "Türkçe", |
||||
|
checkEverySecond: "{0} Saniyede bir kontrol et.", |
||||
|
retriesDescription: "Servisin kapalı olarak işaretlenmeden ve bir bildirim gönderilmeden önce maksimum yeniden deneme sayısı", |
||||
|
ignoreTLSError: "HTTPS web siteleri için TLS/SSL hatasını yoksay", |
||||
|
upsideDownModeDescription: "Servisin durumunu tersine çevirir. Servis çalışıyorsa kapalı olarak işaretler.", |
||||
|
maxRedirectDescription: "İzlenecek maksimum yönlendirme sayısı. Yönlendirmeleri devre dışı bırakmak için 0'a ayarlayın.", |
||||
|
acceptedStatusCodesDescription: "Servisin çalıştığını hangi durum kodları belirlesin?", |
||||
|
passwordNotMatchMsg: "Şifre eşleşmiyor.", |
||||
|
notificationDescription: "Servislerin bildirim gönderebilmesi için bir bildirim yöntemi belirleyin.", |
||||
|
keywordDescription: "Anahtar kelimeyi düz html veya JSON yanıtında arayın ve büyük/küçük harfe duyarlıdır", |
||||
|
pauseDashboardHome: "Durdur", |
||||
|
deleteMonitorMsg: "Servisi silmek istediğinden emin misin?", |
||||
|
deleteNotificationMsg: "Bu bildirimi tüm servisler için silmek istediğinden emin misin?", |
||||
|
resoverserverDescription: "Cloudflare varsayılan sunucudur, çözümleyici sunucusunu istediğiniz zaman değiştirebilirsiniz.", |
||||
|
rrtypeDescription: "İzlemek istediğiniz servisin RR-Tipini seçin", |
||||
|
pauseMonitorMsg: "Durdurmak istediğinden emin misin?", |
||||
|
clearEventsMsg: "Bu servisin bütün kayıtlarını silmek istediğinden emin misin?", |
||||
|
clearHeartbeatsMsg: "Bu servis için tüm sağlık durumunu silmek istediğinden emin misin?", |
||||
|
confirmClearStatisticsMsg: "Tüm istatistikleri silmek istediğinden emin misin?", |
||||
|
Settings: "Ayarlar", |
||||
|
Dashboard: "Panel", |
||||
|
"New Update": "Yeni Güncelleme", |
||||
|
Language: "Dil", |
||||
|
Appearance: "Görünüm", |
||||
|
Theme: "Tema", |
||||
|
General: "Genel", |
||||
|
Version: "Versiyon", |
||||
|
"Check Update On GitHub": "GitHub'da Güncellemeyi Kontrol Edin", |
||||
|
List: "Liste", |
||||
|
Add: "Ekle", |
||||
|
"Add New Monitor": "Yeni Servis Ekle", |
||||
|
"Quick Stats": "Servis istatistikleri", |
||||
|
Up: "Normal", |
||||
|
Down: "Hatalı", |
||||
|
Pending: "Bekliyor", |
||||
|
Unknown: "Bilinmeyen", |
||||
|
Pause: "Durdur", |
||||
|
Name: "Servis ismi", |
||||
|
Status: "Durum", |
||||
|
DateTime: "Zaman", |
||||
|
Message: "Mesaj", |
||||
|
"No important events": "Önemli olay yok", |
||||
|
Resume: "Devam et", |
||||
|
Edit: "Düzenle", |
||||
|
Delete: "Sil", |
||||
|
Current: "Şu anda", |
||||
|
Uptime: "Çalışma zamanı", |
||||
|
"Cert Exp.": "Sertifika Süresi", |
||||
|
days: "günler", |
||||
|
day: "gün", |
||||
|
"-day": "-gün", |
||||
|
hour: "saat", |
||||
|
"-hour": "-saat", |
||||
|
Response: "Cevap Süresi", |
||||
|
Ping: "Ping", |
||||
|
"Monitor Type": "Servis Tipi", |
||||
|
Keyword: "Anahtar Kelime", |
||||
|
"Friendly Name": "Panelde görünecek isim", |
||||
|
URL: "URL", |
||||
|
Hostname: "IP Adresi", |
||||
|
Port: "Port", |
||||
|
"Heartbeat Interval": "Servis Test Aralığı", |
||||
|
Retries: "Yeniden deneme", |
||||
|
Advanced: "Gelişmiş", |
||||
|
"Upside Down Mode": "Ters/Düz Modu", |
||||
|
"Max. Redirects": "Maksimum Yönlendirme", |
||||
|
"Accepted Status Codes": "Kabul Edilen Durum Kodları", |
||||
|
Save: "Kaydet", |
||||
|
Notifications: "Bildirimler", |
||||
|
"Not available, please setup.": "Atanmış bildirim yöntemi yok. Ayarlardan belirleyebilirsiniz.", |
||||
|
"Setup Notification": "Bildirim yöntemi kur", |
||||
|
Light: "Açık", |
||||
|
Dark: "Koyu", |
||||
|
Auto: "Oto", |
||||
|
"Theme - Heartbeat Bar": "Servis Bar Konumu", |
||||
|
Normal: "Normal", |
||||
|
Bottom: "Aşağıda", |
||||
|
None: "Gösterme", |
||||
|
Timezone: "Zaman Dilimi", |
||||
|
"Search Engine Visibility": "Arama Motoru Görünürlüğü", |
||||
|
"Allow indexing": "İndekslemeye izin ver", |
||||
|
"Discourage search engines from indexing site": "İndekslemeyi reddet", |
||||
|
"Change Password": "Şifre Değiştir", |
||||
|
"Current Password": "Şuan ki Şifre", |
||||
|
"New Password": "Yeni Şifre", |
||||
|
"Repeat New Password": "Yeni Şifreyi Tekrar Girin", |
||||
|
"Update Password": "Şifreyi Değiştir", |
||||
|
"Disable Auth": "Şifreli girişi iptal et.", |
||||
|
"Enable Auth": "Şifreli girişi aktif et.", |
||||
|
Logout: "Çıkış yap", |
||||
|
Leave: "Ayrıl", |
||||
|
"I understand, please disable": "Evet farkındayım, iptal et", |
||||
|
Confirm: "Onayla", |
||||
|
Yes: "Evet", |
||||
|
No: "Hayır", |
||||
|
Username: "Kullanıcı Adı", |
||||
|
Password: "Şifre", |
||||
|
"Remember me": "Beni Hatırla", |
||||
|
Login: "Giriş yap", |
||||
|
"No Monitors, please": "Servis yok, lütfen", |
||||
|
"add one": "bir servis ekleyin", |
||||
|
"Notification Type": "Bildirim Yöntemi", |
||||
|
Email: "E-mail", |
||||
|
Test: "Test", |
||||
|
"Certificate Info": "Sertifika Bilgisi", |
||||
|
"Resolver Server": "Çözümleyici Sunucu", |
||||
|
"Resource Record Type": "Kaynak Kayıt Türü", |
||||
|
"Last Result": "En son sonuçlar", |
||||
|
"Create your admin account": "Yönetici hesabınızı oluşturun", |
||||
|
"Repeat Password": "Şifrenizi tekrar girin", |
||||
|
respTime: "Cevap Süresi (ms)", |
||||
|
notAvailableShort: "N/A", |
||||
|
Create: "Yarat", |
||||
|
"Clear Data": "Verileri Temizle", |
||||
|
Events: "Olaylar", |
||||
|
Heartbeats: "Sağlık Durumları", |
||||
|
"Auto Get": "Otomatik Al", |
||||
|
retryCheckEverySecond: "Retry every {0} seconds.", |
||||
|
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", |
||||
|
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.", |
||||
|
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.", |
||||
|
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", |
||||
|
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", |
||||
|
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", |
||||
|
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", |
||||
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval", |
||||
|
"Import Backup": "Import Backup", |
||||
|
"Export Backup": "Export Backup", |
||||
|
Export: "Export", |
||||
|
Import: "Import", |
||||
|
"Default enabled": "Default enabled", |
||||
|
"Apply on all existing monitors": "Apply on all existing monitors", |
||||
|
backupDescription: "You can backup all monitors and all notifications into a JSON file.", |
||||
|
backupDescription2: "PS: History and event data is not included.", |
||||
|
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", |
||||
|
alertNoFile: "Please select a file to import.", |
||||
|
alertWrongFileType: "Please select a JSON file.", |
||||
|
"Clear all statistics": "Clear all Statistics", |
||||
|
"Skip existing": "Skip existing", |
||||
|
Overwrite: "Overwrite", |
||||
|
Options: "Options", |
||||
|
"Keep both": "Keep both", |
||||
|
"Verify Token": "Verify Token", |
||||
|
"Setup 2FA": "Setup 2FA", |
||||
|
"Enable 2FA": "Enable 2FA", |
||||
|
"Disable 2FA": "Disable 2FA", |
||||
|
"2FA Settings": "2FA Settings", |
||||
|
"Two Factor Authentication": "Two Factor Authentication", |
||||
|
Active: "Active", |
||||
|
Inactive: "Inactive", |
||||
|
Token: "Token", |
||||
|
"Show URI": "Show URI", |
||||
|
Tags: "Tags", |
||||
|
"Add New below or Select...": "Add New below or Select...", |
||||
|
"Tag with this name already exist.": "Tag with this name already exist.", |
||||
|
"Tag with this value already exist.": "Tag with this value already exist.", |
||||
|
color: "color", |
||||
|
"value (optional)": "value (optional)", |
||||
|
Gray: "Gray", |
||||
|
Red: "Red", |
||||
|
Orange: "Orange", |
||||
|
Green: "Green", |
||||
|
Blue: "Blue", |
||||
|
Indigo: "Indigo", |
||||
|
Purple: "Purple", |
||||
|
Pink: "Pink", |
||||
|
"Search...": "Search...", |
||||
|
"Avg. Ping": "Avg. Ping", |
||||
|
"Avg. Response": "Avg. Response", |
||||
|
} |
@ -0,0 +1,71 @@ |
|||||
|
import { createRouter, createWebHistory } from "vue-router"; |
||||
|
import EmptyLayout from "./layouts/EmptyLayout.vue"; |
||||
|
import Layout from "./layouts/Layout.vue"; |
||||
|
import Dashboard from "./pages/Dashboard.vue"; |
||||
|
import DashboardHome from "./pages/DashboardHome.vue"; |
||||
|
import Details from "./pages/Details.vue"; |
||||
|
import EditMonitor from "./pages/EditMonitor.vue"; |
||||
|
import List from "./pages/List.vue"; |
||||
|
import Settings from "./pages/Settings.vue"; |
||||
|
import Setup from "./pages/Setup.vue"; |
||||
|
|
||||
|
const routes = [ |
||||
|
{ |
||||
|
path: "/", |
||||
|
component: Layout, |
||||
|
children: [ |
||||
|
{ |
||||
|
name: "root", |
||||
|
path: "", |
||||
|
component: Dashboard, |
||||
|
children: [ |
||||
|
{ |
||||
|
name: "DashboardHome", |
||||
|
path: "/dashboard", |
||||
|
component: DashboardHome, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: "/dashboard/:id", |
||||
|
component: EmptyLayout, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: "", |
||||
|
component: Details, |
||||
|
}, |
||||
|
{ |
||||
|
path: "/edit/:id", |
||||
|
component: EditMonitor, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
path: "/add", |
||||
|
component: EditMonitor, |
||||
|
}, |
||||
|
{ |
||||
|
path: "/list", |
||||
|
component: List, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
path: "/settings", |
||||
|
component: Settings, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
|
||||
|
], |
||||
|
|
||||
|
}, |
||||
|
{ |
||||
|
path: "/setup", |
||||
|
component: Setup, |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
export const router = createRouter({ |
||||
|
linkActiveClass: "active", |
||||
|
history: createWebHistory(), |
||||
|
routes, |
||||
|
}); |
@ -0,0 +1,10 @@ |
|||||
|
FROM ubuntu |
||||
|
WORKDIR /app |
||||
|
RUN apt update && apt --yes install git curl |
||||
|
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - |
||||
|
RUN apt --yes install nodejs |
||||
|
RUN git clone https://github.com/louislam/uptime-kuma.git . |
||||
|
RUN npm run setup |
||||
|
|
||||
|
# Option 1. Try it |
||||
|
RUN node server/server.js |
Loading…
Reference in new issue