mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-21 00:26:47 +08:00
Merge remote-tracking branch 'origin/master' into master-weblate
# Conflicts: # src/lang/pl.json # src/lang/uk-UA.json
This commit is contained in:
@@ -576,6 +576,12 @@ optgroup {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.prism-editor__container {
|
||||
.important {
|
||||
font-weight: var(--bs-body-font-weight) !important;
|
||||
}
|
||||
}
|
||||
|
||||
h5.settings-subheading::after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
152
src/components/EditMonitorCondition.vue
Normal file
152
src/components/EditMonitorCondition.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="monitor-condition mb-3" data-testid="condition">
|
||||
<button
|
||||
v-if="!isInGroup || !isFirst || !isLast"
|
||||
class="btn btn-outline-danger remove-button"
|
||||
type="button"
|
||||
:aria-label="$t('conditionDelete')"
|
||||
data-testid="remove-condition"
|
||||
@click="remove"
|
||||
>
|
||||
<font-awesome-icon icon="trash" />
|
||||
</button>
|
||||
|
||||
<select v-if="!isFirst" v-model="model.andOr" class="form-select and-or-select" data-testid="condition-and-or">
|
||||
<option value="and">{{ $t("and") }}</option>
|
||||
<option value="or">{{ $t("or") }}</option>
|
||||
</select>
|
||||
|
||||
<select v-model="model.variable" class="form-select" data-testid="condition-variable">
|
||||
<option
|
||||
v-for="variable in conditionVariables"
|
||||
:key="variable.id"
|
||||
:value="variable.id"
|
||||
>
|
||||
{{ $t(variable.id) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select v-model="model.operator" class="form-select" data-testid="condition-operator">
|
||||
<option
|
||||
v-for="operator in getVariableOperators(model.variable)"
|
||||
:key="operator.id"
|
||||
:value="operator.id"
|
||||
>
|
||||
{{ $t(operator.caption) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-model="model.value"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:aria-label="$t('conditionValuePlaceholder')"
|
||||
data-testid="condition-value"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "EditMonitorCondition",
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The monitor condition
|
||||
*/
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this is the first condition
|
||||
*/
|
||||
isFirst: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this is the last condition
|
||||
*/
|
||||
isLast: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this condition is in a group
|
||||
*/
|
||||
isInGroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Variable choices
|
||||
*/
|
||||
conditionVariables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [ "update:modelValue", "remove" ],
|
||||
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
remove() {
|
||||
this.$emit("remove", this.model);
|
||||
},
|
||||
|
||||
getVariableOperators(variableId) {
|
||||
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.monitor-condition {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
justify-self: flex-end;
|
||||
margin-bottom: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@container (min-width: 500px) {
|
||||
.monitor-condition {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
order: 100;
|
||||
}
|
||||
|
||||
.and-or-select {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
189
src/components/EditMonitorConditionGroup.vue
Normal file
189
src/components/EditMonitorConditionGroup.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div class="condition-group mb-3" data-testid="condition-group">
|
||||
<div class="d-flex">
|
||||
<select v-if="!isFirst" v-model="model.andOr" class="form-select" style="width: auto;" data-testid="condition-group-and-or">
|
||||
<option value="and">{{ $t("and") }}</option>
|
||||
<option value="or">{{ $t("or") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="condition-group-inner mt-2 pa-2">
|
||||
<div class="condition-group-conditions">
|
||||
<template v-for="(child, childIndex) in model.children" :key="childIndex">
|
||||
<EditMonitorConditionGroup
|
||||
v-if="child.type === 'group'"
|
||||
v-model="model.children[childIndex]"
|
||||
:is-first="childIndex === 0"
|
||||
:get-new-group="getNewGroup"
|
||||
:get-new-condition="getNewCondition"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeChild"
|
||||
/>
|
||||
<EditMonitorCondition
|
||||
v-else
|
||||
v-model="model.children[childIndex]"
|
||||
:is-first="childIndex === 0"
|
||||
:is-last="childIndex === model.children.length - 1"
|
||||
:is-in-group="true"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeChild"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="condition-group-actions mt-3">
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||
{{ $t("conditionAdd") }}
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||
{{ $t("conditionAddGroup") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-danger"
|
||||
type="button"
|
||||
:aria-label="$t('conditionDeleteGroup')"
|
||||
data-testid="remove-condition-group"
|
||||
@click="remove"
|
||||
>
|
||||
<font-awesome-icon icon="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||
|
||||
export default {
|
||||
name: "EditMonitorConditionGroup",
|
||||
|
||||
components: {
|
||||
EditMonitorCondition,
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The condition group
|
||||
*/
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this is the first condition
|
||||
*/
|
||||
isFirst: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to generate a new group model
|
||||
*/
|
||||
getNewGroup: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to generate a new condition model
|
||||
*/
|
||||
getNewCondition: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Variable choices
|
||||
*/
|
||||
conditionVariables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [ "update:modelValue", "remove" ],
|
||||
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addGroup() {
|
||||
const conditions = [ ...this.model.children ];
|
||||
conditions.push(this.getNewGroup());
|
||||
this.model.children = conditions;
|
||||
},
|
||||
|
||||
addCondition() {
|
||||
const conditions = [ ...this.model.children ];
|
||||
conditions.push(this.getNewCondition());
|
||||
this.model.children = conditions;
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.$emit("remove", this.model);
|
||||
},
|
||||
|
||||
removeChild(child) {
|
||||
const idx = this.model.children.indexOf(child);
|
||||
if (idx !== -1) {
|
||||
this.model.children.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.condition-group-inner {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dark .condition-group-inner {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.condition-group-conditions {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.condition-group-actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
// Delete button
|
||||
.condition-group-actions > :last-child {
|
||||
margin-left: auto;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.condition-group-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// Delete button
|
||||
.condition-group-actions > :last-child {
|
||||
margin-left: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn-delete-group {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
149
src/components/EditMonitorConditions.vue
Normal file
149
src/components/EditMonitorConditions.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="monitor-conditions">
|
||||
<label class="form-label">{{ $t("Conditions") }}</label>
|
||||
<div class="monitor-conditions-conditions">
|
||||
<template v-for="(condition, conditionIndex) in model" :key="conditionIndex">
|
||||
<EditMonitorConditionGroup
|
||||
v-if="condition.type === 'group'"
|
||||
v-model="model[conditionIndex]"
|
||||
:is-first="conditionIndex === 0"
|
||||
:get-new-group="getNewGroup"
|
||||
:get-new-condition="getNewCondition"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeCondition"
|
||||
/>
|
||||
<EditMonitorCondition
|
||||
v-else
|
||||
v-model="model[conditionIndex]"
|
||||
:is-first="conditionIndex === 0"
|
||||
:is-last="conditionIndex === model.length - 1"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeCondition"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="monitor-conditions-buttons">
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||
{{ $t("conditionAdd") }}
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||
{{ $t("conditionAddGroup") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditMonitorConditionGroup from "./EditMonitorConditionGroup.vue";
|
||||
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||
|
||||
export default {
|
||||
name: "EditMonitorConditions",
|
||||
|
||||
components: {
|
||||
EditMonitorConditionGroup,
|
||||
EditMonitorCondition,
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The monitor conditions
|
||||
*/
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
conditionVariables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [ "update:modelValue" ],
|
||||
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.model.length === 0) {
|
||||
this.addCondition();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getNewGroup() {
|
||||
return {
|
||||
type: "group",
|
||||
children: [ this.getNewCondition() ],
|
||||
andOr: "and",
|
||||
};
|
||||
},
|
||||
|
||||
getNewCondition() {
|
||||
const firstVariable = this.conditionVariables[0]?.id || null;
|
||||
const firstOperator = this.getVariableOperators(firstVariable)[0] || null;
|
||||
return {
|
||||
type: "expression",
|
||||
variable: firstVariable,
|
||||
operator: firstOperator?.id || null,
|
||||
value: "",
|
||||
andOr: "and",
|
||||
};
|
||||
},
|
||||
|
||||
addGroup() {
|
||||
const conditions = [ ...this.model ];
|
||||
conditions.push(this.getNewGroup());
|
||||
this.$emit("update:modelValue", conditions);
|
||||
},
|
||||
|
||||
addCondition() {
|
||||
const conditions = [ ...this.model ];
|
||||
conditions.push(this.getNewCondition());
|
||||
this.$emit("update:modelValue", conditions);
|
||||
},
|
||||
|
||||
removeCondition(condition) {
|
||||
const conditions = [ ...this.model ];
|
||||
const idx = conditions.indexOf(condition);
|
||||
if (idx !== -1) {
|
||||
conditions.splice(idx, 1);
|
||||
this.$emit("update:modelValue", conditions);
|
||||
}
|
||||
},
|
||||
|
||||
getVariableOperators(variableId) {
|
||||
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.monitor-conditions,
|
||||
.monitor-conditions-conditions {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.monitor-conditions-buttons {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.monitor-conditions-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -14,7 +14,7 @@
|
||||
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
||||
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
||||
>
|
||||
<div>{{ timeSinceFirstBeat }} ago</div>
|
||||
<div>{{ timeSinceFirstBeat }}</div>
|
||||
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
|
||||
<div>{{ timeSinceLastBeat }}</div>
|
||||
</div>
|
||||
@@ -184,11 +184,11 @@ export default {
|
||||
}
|
||||
|
||||
if (seconds < tolerance) {
|
||||
return "now";
|
||||
return this.$t("now");
|
||||
} else if (seconds < 60 * 60) {
|
||||
return (seconds / 60).toFixed(0) + "m ago";
|
||||
return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ]);
|
||||
} else {
|
||||
return (seconds / 60 / 60).toFixed(0) + "h ago";
|
||||
return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -135,6 +135,7 @@ export default {
|
||||
"ntfy": "Ntfy",
|
||||
"octopush": "Octopush",
|
||||
"OneBot": "OneBot",
|
||||
"Onesender": "Onesender",
|
||||
"Opsgenie": "Opsgenie",
|
||||
"PagerDuty": "PagerDuty",
|
||||
"PagerTree": "PagerTree",
|
||||
@@ -144,6 +145,7 @@ export default {
|
||||
"pushy": "Pushy",
|
||||
"rocket.chat": "Rocket.Chat",
|
||||
"signal": "Signal",
|
||||
"SIGNL4": "SIGNL4",
|
||||
"slack": "Slack",
|
||||
"squadcast": "SquadCast",
|
||||
"SMSEagle": "SMSEagle",
|
||||
@@ -178,6 +180,7 @@ export default {
|
||||
"WeCom": "WeCom (企业微信群机器人)",
|
||||
"ServerChan": "ServerChan (Server酱)",
|
||||
"smsc": "SMSC",
|
||||
"WPush": "WPush(wpush.cn)",
|
||||
};
|
||||
|
||||
// Sort by notification name
|
||||
|
@@ -33,7 +33,7 @@
|
||||
<template #item="monitor">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding">
|
||||
<div class="col-6 col-md-4 small-padding">
|
||||
<div class="info">
|
||||
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
|
||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
81
src/components/notifications/Onesender.vue
Normal file
81
src/components/notifications/Onesender.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="host-onesender" class="form-label">{{ $t("Host Onesender") }}</label>
|
||||
<input
|
||||
id="host-onesender"
|
||||
v-model="$parent.notification.onesenderURL"
|
||||
type="url"
|
||||
placeholder="https://xxxxxxxxxxx.com/api/v1/messages"
|
||||
pattern="https?://.+"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="receiver-onesender" class="form-label">{{ $t("Token Onesender") }}</label>
|
||||
<HiddenInput id="receiver-onesender" v-model="$parent.notification.onesenderToken" :required="true" autocomplete="false"></HiddenInput>
|
||||
<i18n-t tag="div" keypath="wayToGetOnesenderUrlandToken" class="form-text">
|
||||
<a href="https://onesender.net/" target="_blank">{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-request-body" class="form-label">{{ $t("Recipient Type") }}</label>
|
||||
<select
|
||||
id="webhook-request-body"
|
||||
v-model="$parent.notification.onesenderTypeReceiver"
|
||||
class="form-select"
|
||||
required
|
||||
>
|
||||
<option value="private">{{ $t("Private Number") }}</option>
|
||||
<option value="group">{{ $t("Group ID") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="$parent.notification.onesenderTypeReceiver == 'private'" class="form-text">{{ $t("privateOnesenderDesc", ['"application/json"']) }}</div>
|
||||
<div v-else class="form-text">{{ $t("groupOnesenderDesc") }}</div>
|
||||
<div class="mb-3">
|
||||
<input
|
||||
id="type-receiver-onesender"
|
||||
v-model="$parent.notification.onesenderReceiver"
|
||||
type="text"
|
||||
placeholder="628123456789 or 628123456789-34534"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input
|
||||
id="type-receiver-onesender"
|
||||
v-model="computedReceiverResult"
|
||||
type="text"
|
||||
class="form-control"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
computedReceiverResult() {
|
||||
let receiver = this.$parent.notification.onesenderReceiver;
|
||||
return this.$parent.notification.onesenderTypeReceiver === "private" ? receiver + "@s.whatsapp.net" : receiver + "@g.us";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
16
src/components/notifications/SIGNL4.vue
Normal file
16
src/components/notifications/SIGNL4.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="signl4-webhook-url" class="form-label">{{ $t("SIGNL4 Webhook URL") }}</label>
|
||||
<input
|
||||
id="signl4-webhook-url"
|
||||
v-model="$parent.notification.webhookURL"
|
||||
type="url"
|
||||
pattern="https?://.+"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
<i18n-t tag="div" keypath="signl4Docs" class="form-text">
|
||||
<a href="https://docs.signl4.com/integrations/uptime-kuma/uptime-kuma.html" target="_blank">SIGNL4 Docs</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
31
src/components/notifications/WPush.vue
Normal file
31
src/components/notifications/WPush.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="wpush-apikey" class="form-label">WPush {{ $t("API Key") }}</label>
|
||||
<HiddenInput id="wpush-apikey" v-model="$parent.notification.wpushAPIkey" :required="true" autocomplete="new-password" placeholder="WPushxxxxx"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="wpush-channel" class="form-label">发送通道</label>
|
||||
<select id="wpush-channel" v-model="$parent.notification.wpushChannel" class="form-select" required>
|
||||
<option value="wechat">微信</option>
|
||||
<option value="sms">短信</option>
|
||||
<option value="mail">邮件</option>
|
||||
<option value="feishu">飞书</option>
|
||||
<option value="dingtalk">钉钉</option>
|
||||
<option value="wechat_work">企业微信</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<i18n-t tag="p" keypath="More info on:">
|
||||
<a href="https://wpush.cn/" rel="noopener noreferrer" target="_blank">https://wpush.cn/</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -29,6 +29,7 @@ import Nostr from "./Nostr.vue";
|
||||
import Ntfy from "./Ntfy.vue";
|
||||
import Octopush from "./Octopush.vue";
|
||||
import OneBot from "./OneBot.vue";
|
||||
import Onesender from "./Onesender.vue";
|
||||
import Opsgenie from "./Opsgenie.vue";
|
||||
import PagerDuty from "./PagerDuty.vue";
|
||||
import FlashDuty from "./FlashDuty.vue";
|
||||
@@ -62,6 +63,8 @@ import Splunk from "./Splunk.vue";
|
||||
import SevenIO from "./SevenIO.vue";
|
||||
import Whapi from "./Whapi.vue";
|
||||
import Cellsynt from "./Cellsynt.vue";
|
||||
import WPush from "./WPush.vue";
|
||||
import SIGNL4 from "./SIGNL4.vue";
|
||||
|
||||
/**
|
||||
* Manage all notification form.
|
||||
@@ -98,6 +101,7 @@ const NotificationFormList = {
|
||||
"ntfy": Ntfy,
|
||||
"octopush": Octopush,
|
||||
"OneBot": OneBot,
|
||||
"Onesender": Onesender,
|
||||
"Opsgenie": Opsgenie,
|
||||
"PagerDuty": PagerDuty,
|
||||
"FlashDuty": FlashDuty,
|
||||
@@ -111,6 +115,7 @@ const NotificationFormList = {
|
||||
"rocket.chat": RocketChat,
|
||||
"serwersms": SerwerSMS,
|
||||
"signal": Signal,
|
||||
"SIGNL4": SIGNL4,
|
||||
"SMSManager": SMSManager,
|
||||
"SMSPartner": SMSPartner,
|
||||
"slack": Slack,
|
||||
@@ -132,6 +137,7 @@ const NotificationFormList = {
|
||||
"whapi": Whapi,
|
||||
"gtxmessaging": GtxMessaging,
|
||||
"Cellsynt": Cellsynt,
|
||||
"WPush": WPush
|
||||
};
|
||||
|
||||
export default NotificationFormList;
|
||||
|
@@ -802,7 +802,6 @@
|
||||
"twilioApiKey": "API ключ (по избор)",
|
||||
"Expected Value": "Очаквана стойност",
|
||||
"Json Query": "Заявка тип JSON",
|
||||
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте {0} за документация относно езика на заявката. Имате възможност да тествате {1}.",
|
||||
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
|
||||
"Badge Preview": "Преглед на баджа",
|
||||
"Notify Channel": "Канал за известяване",
|
||||
|
@@ -823,7 +823,6 @@
|
||||
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
|
||||
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
|
||||
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
|
||||
"jsonQueryDescription": "Proveďte JSON dotaz vůči odpovědi a zkontrolujte očekávaný výstup (za účelem porovnání bude návratová hodnota převedena na řetězec). Dokumentaci k dotazovacímu jazyku naleznete na {0}, a využít můžete též {1}.",
|
||||
"Select": "Vybrat",
|
||||
"selectedMonitorCount": "Vybráno: {0}",
|
||||
"Check/Uncheck": "Vybrat/Zrušit výběr",
|
||||
|
@@ -812,7 +812,6 @@
|
||||
"Json Query": "Json-Abfrage",
|
||||
"filterActive": "Aktiv",
|
||||
"filterActivePaused": "Pausiert",
|
||||
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
|
||||
"Badge Duration (in hours)": "Abzeichen Dauer (in Stunden)",
|
||||
"Badge Preview": "Abzeichen Vorschau",
|
||||
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
||||
|
@@ -817,7 +817,6 @@
|
||||
"filterActivePaused": "Pausiert",
|
||||
"Expected Value": "Erwarteter Wert",
|
||||
"Json Query": "Json-Abfrage",
|
||||
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
|
||||
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
||||
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
|
||||
"pushDeerServerDescription": "Leer lassen um den offiziellen Server zu verwenden",
|
||||
|
@@ -49,17 +49,20 @@
|
||||
"Uptime": "Uptime",
|
||||
"Cert Exp.": "Cert Exp.",
|
||||
"Monitor": "Monitor | Monitors",
|
||||
"now": "now",
|
||||
"time ago": "{0} ago",
|
||||
"day": "day | days",
|
||||
"-day": "-day",
|
||||
"hour": "hour",
|
||||
"-hour": "-hour",
|
||||
"-year": "-year",
|
||||
"Response": "Response",
|
||||
"Ping": "Ping",
|
||||
"Monitor Type": "Monitor Type",
|
||||
"Keyword": "Keyword",
|
||||
"Invert Keyword": "Invert Keyword",
|
||||
"Expected Value": "Expected Value",
|
||||
"Json Query": "Json Query",
|
||||
"Json Query Expression": "Json Query Expression",
|
||||
"Friendly Name": "Friendly Name",
|
||||
"URL": "URL",
|
||||
"Hostname": "Hostname",
|
||||
@@ -80,7 +83,7 @@
|
||||
"resendDisabled": "Resend disabled",
|
||||
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
|
||||
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
@@ -441,6 +444,7 @@
|
||||
"backupOutdatedWarning": "Deprecated: Since a lot of features were added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
|
||||
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
||||
"Optional": "Optional",
|
||||
"and": "and",
|
||||
"or": "or",
|
||||
"sameAsServerTimezone": "Same as Server Timezone",
|
||||
"startDateTime": "Start Date/Time",
|
||||
@@ -588,7 +592,7 @@
|
||||
"notificationDescription": "Notifications must be assigned to a monitor to function.",
|
||||
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
||||
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
|
||||
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out {0} for the documentation about the query language. A playground can be found {1}.",
|
||||
"jsonQueryDescription": "Parse and extract specific data from the server's JSON response using JSON query or use \"$\" for the raw response, if not expecting JSON. The result is then compared to the expected value, as strings. See {0} for documentation and use {1} to experiment with queries.",
|
||||
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
||||
"backupDescription2": "Note: history and event data is not included.",
|
||||
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
||||
@@ -876,6 +880,8 @@
|
||||
"nostrRecipientsHelp": "npub format, one per line",
|
||||
"showCertificateExpiry": "Show Certificate Expiry",
|
||||
"noOrBadCertificate": "No/Bad Certificate",
|
||||
"cacheBusterParam": "Add the {0} parameter",
|
||||
"cacheBusterParamDescription": "Randomly generated parameter to skip caches.",
|
||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
|
||||
"Bitrix24 Webhook URL": "Bitrix24 Webhook URL",
|
||||
@@ -943,6 +949,13 @@
|
||||
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
|
||||
"max 15 digits": "max 15 digits",
|
||||
"max 11 alphanumeric characters": "max 11 alphanumeric characters",
|
||||
"Community String": "Community String",
|
||||
"snmpCommunityStringHelptext": "This string functions as a password to authenticate and control access to SNMP-enabled devices. Match it with your SNMP device's configuration.",
|
||||
"OID (Object Identifier)": "OID (Object Identifier)",
|
||||
"snmpOIDHelptext": "Enter the OID for the sensor or status you want to monitor. Use network management tools like MIB browsers or SNMP software if you're unsure about the OID.",
|
||||
"Condition": "Condition",
|
||||
"SNMP Version": "SNMP Version",
|
||||
"Please enter a valid OID.": "Please enter a valid OID.",
|
||||
"wayToGetThreemaGateway": "You can register for Threema Gateway {0}.",
|
||||
"threemaRecipient": "Recipient",
|
||||
"threemaRecipientType": "Recipient Type",
|
||||
@@ -955,5 +968,51 @@
|
||||
"threemaSenderIdentityFormat": "8 characters, usually starts with *",
|
||||
"threemaApiAuthenticationSecret": "Gateway-ID Secret",
|
||||
"threemaBasicModeInfo": "Note: This integration uses Threema Gateway in basic mode (server-based encryption). Further details can be found {0}.",
|
||||
"apiKeysDisabledMsg": "API keys are disabled because authentication is disabled."
|
||||
"apiKeysDisabledMsg": "API keys are disabled because authentication is disabled.",
|
||||
"Host Onesender": "Host Onesender",
|
||||
"Token Onesender": "Token Onesender",
|
||||
"Recipient Type": "Recipient Type",
|
||||
"Private Number": "Private Number",
|
||||
"privateOnesenderDesc": "Make sure the number phone is valid. To send message into private number phone, ex: 628123456789",
|
||||
"groupOnesenderDesc": "Make sure the GroupID is valid. To send message into Group, ex: 628123456789-342345",
|
||||
"Group ID": "Group ID",
|
||||
"wayToGetOnesenderUrlandToken":"You can get the URL and Token by going to the Onesender website. More info {0}",
|
||||
"Add Remote Browser": "Add Remote Browser",
|
||||
"New Group": "New Group",
|
||||
"Group Name": "Group Name",
|
||||
"OAuth2: Client Credentials": "OAuth2: Client Credentials",
|
||||
"Authentication Method": "Authentication Method",
|
||||
"Authorization Header": "Authorization Header",
|
||||
"Form Data Body": "Form Data Body",
|
||||
"OAuth Token URL": "OAuth Token URL",
|
||||
"Client ID": "Client ID",
|
||||
"Client Secret": "Client Secret",
|
||||
"OAuth Scope": "OAuth Scope",
|
||||
"Optional: Space separated list of scopes": "Optional: Space separated list of scopes",
|
||||
"Go back to home page.": "Go back to home page.",
|
||||
"No tags found.": "No tags found.",
|
||||
"Lost connection to the socket server.": "Lost connection to the socket server.",
|
||||
"Cannot connect to the socket server.": "Cannot connect to the socket server.",
|
||||
"SIGNL4": "SIGNL4",
|
||||
"SIGNL4 Webhook URL": "SIGNL4 Webhook URL",
|
||||
"signl4Docs": "You can find more information about how to configure SIGNL4 and how to obtain the SIGNL4 webhook URL in the {0}.",
|
||||
"Conditions": "Conditions",
|
||||
"conditionAdd": "Add Condition",
|
||||
"conditionDelete": "Delete Condition",
|
||||
"conditionAddGroup": "Add Group",
|
||||
"conditionDeleteGroup": "Delete Group",
|
||||
"conditionValuePlaceholder": "Value",
|
||||
"equals": "equals",
|
||||
"not equals": "not equals",
|
||||
"contains": "contains",
|
||||
"not contains": "not contains",
|
||||
"starts with": "starts with",
|
||||
"not starts with": "not starts with",
|
||||
"ends with": "ends with",
|
||||
"not ends with": "not ends with",
|
||||
"less than": "less than",
|
||||
"greater than": "greater than",
|
||||
"less than or equal to": "less than or equal to",
|
||||
"greater than or equal to": "greater than or equal to",
|
||||
"record": "record"
|
||||
}
|
||||
|
@@ -771,7 +771,6 @@
|
||||
"Json Query": "Consulta Json",
|
||||
"invertKeywordDescription": "Comprobar si la palabra clave está ausente en vez de presente.",
|
||||
"enableNSCD": "Habilitar NSCD (Demonio de Caché de Servicio de Nombres) para almacenar en caché todas las solicitudes DNS",
|
||||
"jsonQueryDescription": "Realiza una consulta JSON contra la respuesta y verifica el valor esperado (el valor de retorno se convertirá a una cadena para la comparación). Consulta {0} para obtener documentación sobre el lenguaje de consulta. Puede encontrar un espacio de prueba {1}.",
|
||||
"Request Timeout": "Tiempo de espera máximo de petición",
|
||||
"timeoutAfter": "Expirar después de {0} segundos",
|
||||
"chromeExecutableDescription": "Para usuarios de Docker, si Chromium no está instalado, puede que tarde unos minutos en ser instalado y mostrar el resultado de la prueba. Usa 1GB de espacio.",
|
||||
|
@@ -759,7 +759,6 @@
|
||||
"filterActive": "فعال",
|
||||
"webhookCustomBodyDesc": "یک بدنه HTTP سفارشی برای ریکوئست تعریف کنید. متغیر های قابل استفاده: {msg}, {heartbeat}, {monitor}.",
|
||||
"tailscalePingWarning": "برای استفاده از Tailscale Ping monitor، شما باید آپتایم کوما را بدون استفاده از داکر و همچنین Tailscale client را نیز بر روی سرور خود نصب داشته باشید.",
|
||||
"jsonQueryDescription": "یک کوئری json در برابر پاسخ انجام دهید و مقدار مورد انتظار را (مقدار برگشتی برای مقایسه به رشته تبدیل می شود). برای مستندات درباره زبان کوئری، {0} مشاهده کنید. همچنین محیط تست را میتوانید در {1} پیدا کنید.",
|
||||
"Enter the list of brokers": "لیست بروکر هارا وارد کنید",
|
||||
"Enable Kafka Producer Auto Topic Creation": "فعال سازی ایجاپ موضوع اتوماتیک تهیه کننده",
|
||||
"Secret AccessKey": "کلید محرمانه AccessKey",
|
||||
|
@@ -792,7 +792,6 @@
|
||||
"emailTemplateLimitedToUpDownNotification": "saatavilla vain YLÖS/ALAS sydämensykkeille, muulloin null",
|
||||
"Your User ID": "Käyttäjätunnuksesi",
|
||||
"invertKeywordDescription": "Etsi puuttuvaa avainsanaa.",
|
||||
"jsonQueryDescription": "Suorita JSON-kysely vastaukselle ja tarkista odotettu arvo (Paluuarvo muutetaan merkkijonoksi vertailua varten). Katso kyselykielen ohjeita osoitteesta {0}. Leikkikenttä löytyy osoitteesta {1}.",
|
||||
"Bark API Version": "Bark API-versio",
|
||||
"Notify Channel": "Ilmoitus kanavalle",
|
||||
"aboutNotifyChannel": "Ilmoitus kanavalle antaa työpöytä- tai mobiili-ilmoituksen kaikille kanavan jäsenille; riippumatta ovatko he paikalla vai poissa.",
|
||||
|
@@ -797,7 +797,6 @@
|
||||
"twilioApiKey": "Clé API (facultatif)",
|
||||
"Expected Value": "Valeur attendue",
|
||||
"Json Query": "Requête Json",
|
||||
"jsonQueryDescription": "Faites une requête json contre la réponse et vérifiez la valeur attendue (la valeur de retour sera convertie en chaîne pour comparaison). Consultez {0} pour la documentation sur le langage de requête. Une aire de jeux peut être trouvée {1}.",
|
||||
"Badge Duration (in hours)": "Durée du badge (en heures)",
|
||||
"Badge Preview": "Aperçu du badge",
|
||||
"aboutNotifyChannel": "Notifier le canal déclenchera une notification de bureau ou mobile pour tous les membres du canal, que leur disponibilité soit active ou absente.",
|
||||
|
@@ -682,7 +682,6 @@
|
||||
"confirmDisableTwoFAMsg": "An bhfuil tú cinnte gur mhaith leat 2FA a dhíchumasú?",
|
||||
"affectedStatusPages": "Taispeáin an teachtaireacht cothabhála seo ar leathanaigh stádais roghnaithe",
|
||||
"keywordDescription": "Cuardaigh eochairfhocal i ngnáthfhreagra HTML nó JSON. Tá an cuardach cás-íogair.",
|
||||
"jsonQueryDescription": "Déan Iarratas json in aghaidh an fhreagra agus seiceáil an luach a bhfuiltear ag súil leis (Déanfar an luach fillte a thiontú ina theaghrán le haghaidh comparáide). Seiceáil {0} le haghaidh na gcáipéisí faoin teanga iarratais. Is féidir clós súgartha a aimsiú {1}.",
|
||||
"backupDescription": "Is féidir leat gach monatóir agus fógra a chúltaca isteach i gcomhad JSON.",
|
||||
"backupDescription2": "Nóta: níl sonraí staire agus imeachta san áireamh.",
|
||||
"octopushAPIKey": "\"Eochair API\" ó dhintiúir API HTTP sa phainéal rialaithe",
|
||||
|
@@ -799,7 +799,6 @@
|
||||
"affectedStatusPages": "Prikazuje poruku o održavanju na odabranim statusnim stranicama",
|
||||
"atLeastOneMonitor": "Odaberite barem jedan zahvaćeni Monitor",
|
||||
"invertKeywordDescription": "Postavi da ključna riječ mora biti odsutna umjesto prisutna.",
|
||||
"jsonQueryDescription": "Izvršite JSON upit nad primljenim odgovorom i provjerite očekivanu povrtanu vrijednost. Ona će se za usporedbu pretvoriti u niz znakova (string). Pogledajte stranicu {0} za dokumentaciju o jeziku upita. Testno okruženje možete pronaći {1}.",
|
||||
"Strategy": "Strategija",
|
||||
"Free Mobile User Identifier": "Besplatni mobilni korisnički identifikator",
|
||||
"Free Mobile API Key": "Besplatni mobilni ključ za API",
|
||||
|
@@ -797,7 +797,6 @@
|
||||
"emailTemplateHeartbeatJSON": "a szívverést leíró objektum",
|
||||
"emailTemplateMsg": "az értesítés üzenete",
|
||||
"emailTemplateLimitedToUpDownNotification": "csak FEL/LE szívverés esetén érhető el, egyébként null érték",
|
||||
"jsonQueryDescription": "Végezzen JSON-lekérdezést a válasz alapján, és ellenőrizze a várt értéket (a visszatérési értéket a rendszer karakterlánccá alakítja az összehasonlításhoz). Nézze meg a {0} webhelyet a lekérdezés paramétereivel kapcsolatos dokumentációért. A test környezet itt található: {1}.",
|
||||
"pushoverMessageTtl": "TTL üzenet (másodperc)",
|
||||
"Platform": "Platform",
|
||||
"aboutNotifyChannel": "A Csatorna értesítése opció, értesítést fog küldeni a csatorna összes tagjának, függetlenül a tagok elérhetőségétől.",
|
||||
|
@@ -838,7 +838,6 @@
|
||||
"emailTemplateHeartbeatJSON": "objek yang menggambarkan heartbeat",
|
||||
"emailTemplateMsg": "pesan pemberitahuan",
|
||||
"emailCustomBody": "Kustomisasi Body",
|
||||
"jsonQueryDescription": "Lakukan Query json terhadap respons dan periksa nilai yang diharapkan (Nilai yang dikembalikan akan diubah menjadi string untuk perbandingan). Lihat {0} untuk dokumentasi tentang bahasa kueri. Taman bermain dapat ditemukan {1}.",
|
||||
"Notify Channel": "Beritahu Saluran",
|
||||
"Server URL should not contain the nfty topic": "URL server tidak boleh berisi topik nfty",
|
||||
"PushDeer Server": "Server PushDeer",
|
||||
|
@@ -620,7 +620,6 @@
|
||||
"enableNSCD": "Abilita NSCD (Name Service Cache Daemon) per abilitare la cache su tutte le richieste DNS",
|
||||
"recurringIntervalMessage": "Esegui una volta al giorno | Esegui una volta ogni {0} giorni",
|
||||
"affectedMonitorsDescription": "Seleziona i monitoraggi che sono influenzati da questa manutenzione",
|
||||
"jsonQueryDescription": "Fai una query JSON verso la risposta e controlla se è presente il valore richiesto. (Il valore di ritorno verrà convertito in stringa ai fini della comparazione). Puoi controllare la documentazione su <a href='https://jsonata.org/'>jsonata.org</a> per conoscere come scrivere una query. Un area dimostrativa può essere trovata <a href='https://try.jsonata.org/'>qui</a>.",
|
||||
"For safety, must use secret key": "Per sicurezza, devi usare una chiave segreta",
|
||||
"Proxy server has authentication": "Il server Proxy ha una autenticazione",
|
||||
"smseaglePriority": "Priorità messaggio (0-9, default = 0)",
|
||||
|
@@ -804,7 +804,6 @@
|
||||
"Reconnecting...": "Opnieuw verbinden...",
|
||||
"Expected Value": "Verwachte waarde",
|
||||
"Json Query": "Json zoekopdracht",
|
||||
"jsonQueryDescription": "Voer een JSON-query uit op de respons en controleer de verwachte waarde (De retourwaarde wordt omgezet naar een string voor vergelijking). Bekijk {0} voor de documentatie over de querytaal. Een speelplaats is beschikbaar {1}.",
|
||||
"pushViewCode": "Hoe gebruik je Push monitor?(View Code)",
|
||||
"setupDatabaseChooseDatabase": "Welke database wil je gebruiken?",
|
||||
"setupDatabaseEmbeddedMariaDB": "Je hoeft niks in te stellen. Dit docker image heeft een ingebouwde en geconfigureerde MariaDB instantie. Uptime Kuma verbindt met deze database via een unix socket.",
|
||||
|
@@ -793,7 +793,6 @@
|
||||
"styleElapsedTime": "Czas, który upłynął pod paskiem bicia serca",
|
||||
"tailscalePingWarning": "Aby korzystać z monitora Tailscale Ping, należy zainstalować Uptime Kuma bez Dockera, a także zainstalować klienta Tailscale na serwerze.",
|
||||
"invertKeywordDescription": "Słowo kluczowe powinno być raczej nieobecne niż obecne.",
|
||||
"jsonQueryDescription": "Wykonaj zapytanie JSON względem odpowiedzi i sprawdź oczekiwaną wartość (wartość zwracana zostanie przekonwertowana na ciąg znaków do porównania). Sprawdź {0}, aby uzyskać dokumentację dotyczącą języka zapytań. Plac zabaw można znaleźć {1}.",
|
||||
"Server URL should not contain the nfty topic": "Adres URL serwera nie powinien zawierać tematu nfty",
|
||||
"Badge Duration (in hours)": "Czas trwania odznaki (w godzinach)",
|
||||
"Enter the list of brokers": "Wprowadź listę brokerów",
|
||||
|
@@ -605,7 +605,6 @@
|
||||
"wayToGetLineChannelToken": "Primeiro acesse o {0}, crie um provedor e um canal (API de Mensagens), então você pode obter o token de acesso do canal e o ID do usuário nos itens de menu mencionados acima.",
|
||||
"aboutMattermostChannelName": "Você pode substituir o canal padrão para o qual o Webhook envia postagens, inserindo o nome do canal no campo \"Nome do Canal\". Isso precisa ser habilitado nas configurações do Webhook do Mattermost. Por exemplo: #outro-canal",
|
||||
"invertKeywordDescription": "Procure pela palavra-chave estar ausente em vez de presente.",
|
||||
"jsonQueryDescription": "Faça uma consulta JSON na resposta e verifique o valor esperado (o valor de retorno será convertido em uma string para comparação). Confira {0} para a documentação sobre a linguagem de consulta. Você pode encontrar um playground {1}.",
|
||||
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
|
||||
"octopushTypeLowCost": "Baixo Custo (Lento - às vezes bloqueado pelo operador)",
|
||||
"octopushSMSSender": "Nome do Remetente de SMS: 3-11 caracteres alfanuméricos e espaço (a-zA-Z0-9)",
|
||||
|
@@ -689,7 +689,6 @@
|
||||
"emailTemplateLimitedToUpDownNotification": "disponibil numai pentru heartbeat-uri UP/DOWN, altfel nul",
|
||||
"emailTemplateStatus": "Stare",
|
||||
"invertKeywordDescription": "Căutați după cuvântul cheie să fie absent și nu prezent.",
|
||||
"jsonQueryDescription": "Efectuați o interogare json după răspuns și verificați valoarea așteptată (valoarea returnată va fi convertită în șir pentru comparație). Consultați {0} pentru documentația despre limbajul de interogare. Un playground poate fi găsit {1}.",
|
||||
"goAlertInfo": "GoAlert este o aplicație open source pentru programarea apelurilor, escalări automate și notificări (cum ar fi SMS-uri sau apeluri vocale). Angajați automat persoana potrivită, în modul potrivit și la momentul potrivit! {0}",
|
||||
"goAlertIntegrationKeyInfo": "Obțineți cheia generică de integrare API pentru serviciu în formatul \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" de obicei valoarea parametrului token al URL-ului copiat.",
|
||||
"SecretAccessKey": "Secret AccessKey",
|
||||
|
@@ -812,7 +812,6 @@
|
||||
"AccessKey Id": "AccessKey Id",
|
||||
"Secret AccessKey": "Секретный ключ доступа",
|
||||
"Session Token": "Токен сессии",
|
||||
"jsonQueryDescription": "Выполните json-запрос к ответу и проверьте наличие ожидаемого значения (возвращаемое значение будет преобразовано в строку для сравнения). Посмотрите {0} для получения документации по языку запросов. A Потренироваться вы можете {1}.",
|
||||
"Notify Channel": "Канал оповещений",
|
||||
"aboutNotifyChannel": "Уведомление о канале вызовет настольное или мобильное уведомление для всех участников канала, независимо от того, установлена ли их доступность как активная или отсутствующая.",
|
||||
"Enter the list of brokers": "Введите список брокеров",
|
||||
|
@@ -795,7 +795,6 @@
|
||||
"pushoverDesc1": "Nödprioritet (2) har 30 sekunders timeout mellan försök och löper ut efter 1 timme som standard.",
|
||||
"octopushTypePremium": "Premium (Snabb - rekommenderas för varningar)",
|
||||
"octopushTypeLowCost": "Låg kostnad (långsam - blockeras ibland av operatören)",
|
||||
"jsonQueryDescription": "Gör en json-förfrågan mot svaret och kontrollera det förväntade värdet (returvärde konverteras till en sträng för jämförelse). Se {0} för dokumentation angående frågespråket. En lekplats kan hittas här {1}.",
|
||||
"Check octopush prices": "Kontrollera octopush priser {0}.",
|
||||
"octopushSMSSender": "SMS avsändarnamn: 3-11 alfanumeriska tecken och mellanslag (a-zA-Z0-9)",
|
||||
"LunaSea Device ID": "LunaSea enhetsid",
|
||||
|
@@ -794,7 +794,6 @@
|
||||
"webhookBodyPresetOption": "Ön ayar - {0}",
|
||||
"webhookBodyCustomOption": "Özel Gövde",
|
||||
"Request Body": "İstek Gövdesi",
|
||||
"jsonQueryDescription": "Yanıta karşı bir json sorgusu yapın ve beklenen değeri kontrol edin (Dönüş değeri, karşılaştırma için dizgeye dönüştürülür). Sorgu diliyle ilgili belgeler için {0}'a bakın. Bir oyun alanı {1} bulunabilir.",
|
||||
"twilioApiKey": "Api Anahtarı (isteğe bağlı)",
|
||||
"Expected Value": "Beklenen Değer",
|
||||
"Json Query": "Json Sorgusu",
|
||||
|
@@ -802,7 +802,6 @@
|
||||
"Request Body": "Тіло запиту",
|
||||
"Badge Preview": "Попередній перегляд бейджа",
|
||||
"Badge Duration (in hours)": "Тривалість бейджа (у годинах)",
|
||||
"jsonQueryDescription": "Виконувати JSON-запит до відповіді та перевірити очікуване значення (значення, що повертається, буде перетворено в рядок для порівняння). Зверніться до {0} щоб ознайомитися з документацією про мову запитів. Навчальний майданчик можна знайти {1}.",
|
||||
"twilioApiKey": "Api ключ (необов'язково)",
|
||||
"Expected Value": "Очікуване значення",
|
||||
"Json Query": "Json-запит",
|
||||
|
@@ -802,7 +802,6 @@
|
||||
"webhookCustomBodyDesc": "为 webhook 设定一个自定义 HTTP 请求体。可在模板内使用 {msg}、{heartbeat}和{monitor} 变量。",
|
||||
"webhookBodyPresetOption": "预设 - {0}",
|
||||
"Request Body": "请求体",
|
||||
"jsonQueryDescription": "对响应结果执行一次 JSON 查询,其返回值将会被转换为字符串,再与期望值进行比较。可访问 {0} 阅读 JSON 查询语言的文档,或在{1}测试查询语句。",
|
||||
"Json Query": "JSON 查询",
|
||||
"twilioApiKey": "API Key(可选)",
|
||||
"Expected Value": "预期值",
|
||||
|
@@ -772,7 +772,6 @@
|
||||
"Check/Uncheck": "選中/取消選中",
|
||||
"tailscalePingWarning": "如需使用 Tailscale Ping 客戶端,您需要以非 docker 方式安裝 Uptime Kuma,並同時安裝 Tailscale 客戶端。",
|
||||
"invertKeywordDescription": "出現關鍵詞將令檢測結果設為失敗,而非成功。",
|
||||
"jsonQueryDescription": "對回應結果執行一次 JSON 查詢,其返回值將會被轉換為字串,再與期望值進行比較。可造訪{0}閱讀JSON 查詢語言的文件,或在{1}測試查詢語句。",
|
||||
"wayToGetKookGuildID": "在 Kook 設定中打開“開發者模式”,然後右鍵點選頻道可取得其 ID",
|
||||
"Notify Channel": "通知該頻道",
|
||||
"aboutNotifyChannel": "勾選“通知該頻道”,會令該頻道內所有成員都收到一條桌面端或移動端通知,無論其狀態是在線或離開。",
|
||||
|
@@ -38,6 +38,7 @@ export default {
|
||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||
loggedIn: false,
|
||||
monitorList: { },
|
||||
monitorTypeList: {},
|
||||
maintenanceList: {},
|
||||
apiKeyList: {},
|
||||
heartbeatList: { },
|
||||
@@ -153,6 +154,10 @@ export default {
|
||||
this.monitorList = data;
|
||||
});
|
||||
|
||||
socket.on("monitorTypeList", (data) => {
|
||||
this.monitorTypeList = data;
|
||||
});
|
||||
|
||||
socket.on("maintenanceList", (data) => {
|
||||
this.maintenanceList = data;
|
||||
});
|
||||
@@ -251,7 +256,7 @@ export default {
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("disconnect");
|
||||
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||
this.connectionErrorMsg = `${this.$t("Lost connection to the socket server.")} ${this.$t("Reconnecting...")}`;
|
||||
this.socket.connected = false;
|
||||
});
|
||||
|
||||
|
@@ -57,7 +57,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
|
||||
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ $root.monitorList[beat.monitorID]?.name }}</router-link></td>
|
||||
<td class="name-column"><router-link :to="`/dashboard/${beat.monitorID}`">{{ $root.monitorList[beat.monitorID]?.name }}</router-link></td>
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
||||
<td class="border-0">{{ beat.msg }}</td>
|
||||
@@ -233,4 +233,16 @@ table {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
.name-column {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-aspect-ratio: 4/3) {
|
||||
.name-column {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -79,7 +79,7 @@
|
||||
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ status.text }}</span>
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;" data-testid="monitor-status">{{ status.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="my-3">
|
||||
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
|
||||
<select id="type" v-model="monitor.type" class="form-select">
|
||||
<select id="type" v-model="monitor.type" class="form-select" data-testid="monitor-type-select">
|
||||
<optgroup :label="$t('General Monitor Type')">
|
||||
<option value="group">
|
||||
{{ $t("Group") }}
|
||||
@@ -24,6 +24,9 @@
|
||||
<option value="ping">
|
||||
Ping
|
||||
</option>
|
||||
<option value="snmp">
|
||||
SNMP
|
||||
</option>
|
||||
<option value="keyword">
|
||||
HTTP(s) - {{ $t("Keyword") }}
|
||||
</option>
|
||||
@@ -96,7 +99,7 @@
|
||||
<!-- Friendly Name -->
|
||||
<div class="my-3">
|
||||
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
|
||||
<input id="name" v-model="monitor.name" type="text" class="form-control" required data-testid="friendly-name-input">
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
@@ -168,21 +171,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Json Query -->
|
||||
<div v-if="monitor.type === 'json-query'" class="my-3">
|
||||
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
||||
|
||||
<i18n-t tag="div" class="form-text" keypath="jsonQueryDescription">
|
||||
<a href="https://jsonata.org/">jsonata.org</a>
|
||||
<a href="https://try.jsonata.org/">{{ $t('here') }}</a>
|
||||
</i18n-t>
|
||||
<br>
|
||||
|
||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<!-- Game -->
|
||||
<!-- GameDig only -->
|
||||
<div v-if="monitor.type === 'gamedig'" class="my-3">
|
||||
@@ -246,19 +234,87 @@
|
||||
</template>
|
||||
|
||||
<!-- Hostname -->
|
||||
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping'" class="my-3">
|
||||
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
|
||||
<input
|
||||
id="hostname"
|
||||
v-model="monitor.hostname"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`"
|
||||
required
|
||||
data-testid="hostname-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Port -->
|
||||
<!-- For TCP Port / Steam / MQTT / Radius Type -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
|
||||
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp'" class="my-3">
|
||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<!-- SNMP Monitor Type -->
|
||||
<div v-if="monitor.type === 'snmp'" class="my-3">
|
||||
<label for="snmp_community_string" class="form-label">{{ $t("Community String") }}</label>
|
||||
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
|
||||
<HiddenInput id="snmp_community_string" v-model="monitor.radiusPassword" autocomplete="false" required="true" placeholder="public"></HiddenInput>
|
||||
|
||||
<div class="form-text">{{ $t('snmpCommunityStringHelptext') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'snmp'" class="my-3">
|
||||
<label for="snmp_oid" class="form-label">{{ $t("OID (Object Identifier)") }}</label>
|
||||
<input id="snmp_oid" v-model="monitor.snmpOid" :title="$t('Please enter a valid OID.') + ' ' + $t('Example:', ['1.3.6.1.4.1.9.6.1.101'])" type="text" class="form-control" pattern="^([0-2])((\.0)|(\.[1-9][0-9]*))*$" placeholder="1.3.6.1.4.1.9.6.1.101" required>
|
||||
<div class="form-text">{{ $t('snmpOIDHelptext') }} </div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'snmp'" class="my-3">
|
||||
<label for="snmp_version" class="form-label">{{ $t("SNMP Version") }}</label>
|
||||
<select id="snmp_version" v-model="monitor.snmpVersion" class="form-select">
|
||||
<option value="1">
|
||||
SNMPv1
|
||||
</option>
|
||||
<option value="2c">
|
||||
SNMPv2c
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Json Query -->
|
||||
<!-- For Json Query / SNMP -->
|
||||
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
||||
<div class="my-2">
|
||||
<label for="jsonPath" class="form-label mb-0">{{ $t("Json Query Expression") }}</label>
|
||||
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
|
||||
<a href="https://jsonata.org/">jsonata.org</a>
|
||||
<a href="https://try.jsonata.org/">{{ $t('playground') }}</a>
|
||||
</i18n-t>
|
||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" placeholder="$" required>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="me-2">
|
||||
<label for="json_path_operator" class="form-label">{{ $t("Condition") }}</label>
|
||||
<select id="json_path_operator" v-model="monitor.jsonPathOperator" class="form-select me-3" required>
|
||||
<option value=">">></option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<"><</option>
|
||||
<option value="<="><=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value="==">==</option>
|
||||
<option value="contains">contains</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||
<input v-if="monitor.jsonPathOperator !== 'contains' && monitor.jsonPathOperator !== '==' && monitor.jsonPathOperator !== '!='" id="expectedValue" v-model="monitor.expectedValue" type="number" class="form-control" required step=".01">
|
||||
<input v-else id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Resolver Server -->
|
||||
<!-- For DNS Type -->
|
||||
<template v-if="monitor.type === 'dns'">
|
||||
@@ -295,6 +351,7 @@
|
||||
:preselect-first="false"
|
||||
:max-height="500"
|
||||
:taggable="false"
|
||||
data-testid="resolve-type-select"
|
||||
></VueMultiselect>
|
||||
|
||||
<div class="form-text">
|
||||
@@ -461,6 +518,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Conditions -->
|
||||
<EditMonitorConditions
|
||||
v-if="supportsConditions && conditionVariables.length > 0"
|
||||
v-model="monitor.conditions"
|
||||
:condition-variables="conditionVariables"
|
||||
class="my-3"
|
||||
/>
|
||||
|
||||
<!-- Interval -->
|
||||
<div class="my-3">
|
||||
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
|
||||
@@ -483,8 +548,8 @@
|
||||
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
|
||||
</div>
|
||||
|
||||
<!-- Timeout: HTTP / Keyword only -->
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'" class="my-3">
|
||||
<!-- Timeout: HTTP / Keyword / SNMP only -->
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
||||
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
|
||||
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
|
||||
</div>
|
||||
@@ -516,6 +581,18 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
|
||||
<input id="cache-bust" v-model="monitor.cacheBust" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="cache-bust">
|
||||
<i18n-t tag="label" keypath="cacheBusterParam" class="form-check-label" for="cache-bust">
|
||||
<code>uptime_kuma_cachebuster</code>
|
||||
</i18n-t>
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("cacheBusterParamDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
@@ -903,7 +980,15 @@
|
||||
</div>
|
||||
|
||||
<div class="fixed-bottom-bar p-3">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
<button
|
||||
id="monitor-submit-btn"
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
:disabled="processing"
|
||||
data-testid="save-button"
|
||||
>
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -912,7 +997,7 @@
|
||||
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
||||
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
||||
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
|
||||
<RemoteBrowserDialog ref="remoteBrowserDialog" @added="addedRemoteBrowser" />
|
||||
<RemoteBrowserDialog ref="remoteBrowserDialog" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -931,6 +1016,7 @@ import TagsManager from "../components/TagsManager.vue";
|
||||
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
|
||||
import { hostNameRegexPattern } from "../util-frontend";
|
||||
import HiddenInput from "../components/HiddenInput.vue";
|
||||
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
|
||||
|
||||
const toast = useToast;
|
||||
|
||||
@@ -946,7 +1032,6 @@ const monitorDefaults = {
|
||||
retryInterval: 60,
|
||||
resendInterval: 0,
|
||||
maxretries: 0,
|
||||
timeout: 48,
|
||||
notificationIDList: {},
|
||||
ignoreTls: false,
|
||||
upsideDown: false,
|
||||
@@ -971,10 +1056,12 @@ const monitorDefaults = {
|
||||
kafkaProducerSaslOptions: {
|
||||
mechanism: "None",
|
||||
},
|
||||
cacheBust: false,
|
||||
kafkaProducerSsl: false,
|
||||
kafkaProducerAllowAutoTopicCreation: false,
|
||||
gamedigGivenPortOnly: true,
|
||||
remote_browser: null
|
||||
remote_browser: null,
|
||||
conditions: []
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -989,6 +1076,7 @@ export default {
|
||||
RemoteBrowserDialog,
|
||||
TagsManager,
|
||||
VueMultiselect,
|
||||
EditMonitorConditions,
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -1158,8 +1246,8 @@ message HealthCheckResponse {
|
||||
// Only groups, not itself, not a decendant
|
||||
result = result.filter(
|
||||
monitor => monitor.type === "group" &&
|
||||
monitor.id !== this.monitor.id &&
|
||||
!this.monitor.childrenIDs?.includes(monitor.id)
|
||||
monitor.id !== this.monitor.id &&
|
||||
!this.monitor.childrenIDs?.includes(monitor.id)
|
||||
);
|
||||
|
||||
// Filter result by active state, weight and alphabetical
|
||||
@@ -1243,7 +1331,15 @@ message HealthCheckResponse {
|
||||
value: null,
|
||||
}];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
supportsConditions() {
|
||||
return this.$root.monitorTypeList[this.monitor.type]?.supportsConditions || false;
|
||||
},
|
||||
|
||||
conditionVariables() {
|
||||
return this.$root.monitorTypeList[this.monitor.type]?.conditionVariables || [];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"$root.proxyList"() {
|
||||
@@ -1276,7 +1372,7 @@ message HealthCheckResponse {
|
||||
}
|
||||
},
|
||||
|
||||
"monitor.type"() {
|
||||
"monitor.type"(newType, oldType) {
|
||||
if (this.monitor.type === "push") {
|
||||
if (! this.monitor.pushToken) {
|
||||
// ideally this would require checking if the generated token is already used
|
||||
@@ -1291,11 +1387,35 @@ message HealthCheckResponse {
|
||||
this.monitor.port = "53";
|
||||
} else if (this.monitor.type === "radius") {
|
||||
this.monitor.port = "1812";
|
||||
} else if (this.monitor.type === "snmp") {
|
||||
this.monitor.port = "161";
|
||||
} else {
|
||||
this.monitor.port = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.monitor.type === "snmp") {
|
||||
// snmp is not expected to be executed via the internet => we can choose a lower default timeout
|
||||
this.monitor.timeout = 5;
|
||||
} else {
|
||||
this.monitor.timeout = 48;
|
||||
}
|
||||
|
||||
// Set default SNMP version
|
||||
if (!this.monitor.snmpVersion) {
|
||||
this.monitor.snmpVersion = "2c";
|
||||
}
|
||||
|
||||
// Set default jsonPath
|
||||
if (!this.monitor.jsonPath) {
|
||||
this.monitor.jsonPath = "$";
|
||||
}
|
||||
|
||||
// Set default condition for for jsonPathOperator
|
||||
if (!this.monitor.jsonPathOperator) {
|
||||
this.monitor.jsonPathOperator = "==";
|
||||
}
|
||||
|
||||
// Get the game list from server
|
||||
if (this.monitor.type === "gamedig") {
|
||||
this.$root.getSocket().emit("getGameList", (res) => {
|
||||
@@ -1324,6 +1444,10 @@ message HealthCheckResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset conditions since condition variables likely change:
|
||||
if (oldType && newType !== oldType) {
|
||||
this.monitor.conditions = [];
|
||||
}
|
||||
},
|
||||
|
||||
currentGameObject(newGameObject, previousGameObject) {
|
||||
|
59
src/util.js
59
src/util.js
@@ -14,8 +14,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
var _a;
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
|
||||
exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0;
|
||||
exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0;
|
||||
const dayjs_1 = __importDefault(require("dayjs"));
|
||||
const dayjs = require("dayjs");
|
||||
const jsonata = require("jsonata");
|
||||
exports.isDev = process.env.NODE_ENV === "development";
|
||||
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
|
||||
exports.appName = "Uptime Kuma";
|
||||
@@ -399,3 +402,59 @@ function intHash(str, length = 10) {
|
||||
return (hash % length + length) % length;
|
||||
}
|
||||
exports.intHash = intHash;
|
||||
async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) {
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(data);
|
||||
}
|
||||
catch (_a) {
|
||||
response = (typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
|
||||
}
|
||||
try {
|
||||
response = (jsonPath) ? await jsonata(jsonPath).evaluate(response) : response;
|
||||
if (response === null || response === undefined) {
|
||||
throw new Error("Empty or undefined response. Check query syntax and response structure");
|
||||
}
|
||||
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
|
||||
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
|
||||
}
|
||||
let jsonQueryExpression;
|
||||
switch (jsonPathOperator) {
|
||||
case ">":
|
||||
case ">=":
|
||||
case "<":
|
||||
case "<=":
|
||||
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
|
||||
break;
|
||||
case "!=":
|
||||
jsonQueryExpression = "$.value != $.expected";
|
||||
break;
|
||||
case "==":
|
||||
jsonQueryExpression = "$.value = $.expected";
|
||||
break;
|
||||
case "contains":
|
||||
jsonQueryExpression = "$contains($.value, $.expected)";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
||||
}
|
||||
const expression = jsonata(jsonQueryExpression);
|
||||
const status = await expression.evaluate({
|
||||
value: response.toString(),
|
||||
expected: expectedValue.toString()
|
||||
});
|
||||
if (status === undefined) {
|
||||
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
|
||||
}
|
||||
return {
|
||||
status,
|
||||
response
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
response = JSON.stringify(response);
|
||||
response = (response && response.length > 50) ? `${response.substring(0, 100)}… (truncated)` : response;
|
||||
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
|
||||
}
|
||||
}
|
||||
exports.evaluateJsonQuery = evaluateJsonQuery;
|
||||
|
75
src/util.ts
75
src/util.ts
@@ -17,6 +17,8 @@ import * as timezone from "dayjs/plugin/timezone";
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import * as utc from "dayjs/plugin/utc";
|
||||
|
||||
import * as jsonata from "jsonata";
|
||||
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
export const isNode = typeof process !== "undefined" && process?.versions?.node;
|
||||
export const appName = "Uptime Kuma";
|
||||
@@ -643,3 +645,76 @@ export function intHash(str : string, length = 10) : number {
|
||||
return (hash % length + length) % length; // Ensure the result is non-negative
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a JSON query expression against the provided data.
|
||||
* @param data The data to evaluate the JSON query against.
|
||||
* @param jsonPath The JSON path or custom JSON query expression.
|
||||
* @param jsonPathOperator The operator to use for comparison.
|
||||
* @param expectedValue The expected value to compare against.
|
||||
* @returns An object containing the status and the evaluation result.
|
||||
* @throws Error if the evaluation returns undefined.
|
||||
*/
|
||||
export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOperator: string, expectedValue: any): Promise<{ status: boolean; response: any }> {
|
||||
// Attempt to parse data as JSON; if unsuccessful, handle based on data type.
|
||||
let response: any;
|
||||
try {
|
||||
response = JSON.parse(data);
|
||||
} catch {
|
||||
response = (typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
|
||||
}
|
||||
|
||||
try {
|
||||
// If a JSON path is provided, pre-evaluate the data using it.
|
||||
response = (jsonPath) ? await jsonata(jsonPath).evaluate(response) : response;
|
||||
|
||||
if (response === null || response === undefined) {
|
||||
throw new Error("Empty or undefined response. Check query syntax and response structure");
|
||||
}
|
||||
|
||||
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
|
||||
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
|
||||
}
|
||||
|
||||
// Perform the comparison logic using the chosen operator
|
||||
let jsonQueryExpression;
|
||||
switch (jsonPathOperator) {
|
||||
case ">":
|
||||
case ">=":
|
||||
case "<":
|
||||
case "<=":
|
||||
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
|
||||
break;
|
||||
case "!=":
|
||||
jsonQueryExpression = "$.value != $.expected";
|
||||
break;
|
||||
case "==":
|
||||
jsonQueryExpression = "$.value = $.expected";
|
||||
break;
|
||||
case "contains":
|
||||
jsonQueryExpression = "$contains($.value, $.expected)";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
||||
}
|
||||
|
||||
// Evaluate the JSON Query Expression
|
||||
const expression = jsonata(jsonQueryExpression);
|
||||
const status = await expression.evaluate({
|
||||
value: response.toString(),
|
||||
expected: expectedValue.toString()
|
||||
});
|
||||
|
||||
if (status === undefined) {
|
||||
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
|
||||
}
|
||||
|
||||
return {
|
||||
status, // The evaluation of the json query
|
||||
response // The response from the server or result from initial json-query evaluation
|
||||
};
|
||||
} catch (err: any) {
|
||||
response = JSON.stringify(response); // Ensure the response is treated as a string for the console
|
||||
response = (response && response.length > 50) ? `${response.substring(0, 100)}… (truncated)` : response;// Truncate long responses to the console
|
||||
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user