mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-19 18:56:48 +02:00
Merge branch 'master' into fix/maintenance_drift
This commit is contained in:
commit
f0abbe0861
17 changed files with 455 additions and 114 deletions
13
db/knex_migrations/2025-06-03-0000-add-ip-family.js
Normal file
13
db/knex_migrations/2025-06-03-0000-add-ip-family.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.boolean("ip_family").defaultTo(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("ip_family");
|
||||||
|
});
|
||||||
|
};
|
12
db/knex_migrations/2025-06-11-0000-add-manual-monitor.js
Normal file
12
db/knex_migrations/2025-06-11-0000-add-manual-monitor.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.string("manual_status").defaultTo(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("manual_status");
|
||||||
|
});
|
||||||
|
};
|
|
@ -158,12 +158,22 @@ class Maintenance extends BeanModel {
|
||||||
bean.active = obj.active;
|
bean.active = obj.active;
|
||||||
|
|
||||||
if (obj.dateRange[0]) {
|
if (obj.dateRange[0]) {
|
||||||
|
const parsedDate = new Date(obj.dateRange[0]);
|
||||||
|
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
|
||||||
|
throw new Error("Invalid start date");
|
||||||
|
}
|
||||||
|
|
||||||
bean.start_date = obj.dateRange[0];
|
bean.start_date = obj.dateRange[0];
|
||||||
} else {
|
} else {
|
||||||
bean.start_date = null;
|
bean.start_date = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.dateRange[1]) {
|
if (obj.dateRange[1]) {
|
||||||
|
const parsedDate = new Date(obj.dateRange[1]);
|
||||||
|
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
|
||||||
|
throw new Error("Invalid end date");
|
||||||
|
}
|
||||||
|
|
||||||
bean.end_date = obj.dateRange[1];
|
bean.end_date = obj.dateRange[1];
|
||||||
} else {
|
} else {
|
||||||
bean.end_date = null;
|
bean.end_date = null;
|
||||||
|
@ -235,7 +245,7 @@ class Maintenance extends BeanModel {
|
||||||
try {
|
try {
|
||||||
this.beanMeta.status = "scheduled";
|
this.beanMeta.status = "scheduled";
|
||||||
|
|
||||||
let startEvent = (customDuration = 0) => {
|
let startEvent = async (customDuration = 0) => {
|
||||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||||
|
|
||||||
this.beanMeta.status = "under-maintenance";
|
this.beanMeta.status = "under-maintenance";
|
||||||
|
|
|
@ -160,6 +160,7 @@ class Monitor extends BeanModel {
|
||||||
smtpSecurity: this.smtpSecurity,
|
smtpSecurity: this.smtpSecurity,
|
||||||
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
|
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
|
||||||
conditions: JSON.parse(this.conditions),
|
conditions: JSON.parse(this.conditions),
|
||||||
|
ipFamily: this.ipFamily,
|
||||||
|
|
||||||
// ping advanced options
|
// ping advanced options
|
||||||
ping_numeric: this.isPingNumeric(),
|
ping_numeric: this.isPingNumeric(),
|
||||||
|
@ -426,10 +427,26 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let agentFamily = undefined;
|
||||||
|
if (this.ipFamily === "ipv4") {
|
||||||
|
agentFamily = 4;
|
||||||
|
}
|
||||||
|
if (this.ipFamily === "ipv6") {
|
||||||
|
agentFamily = 6;
|
||||||
|
}
|
||||||
|
|
||||||
const httpsAgentOptions = {
|
const httpsAgentOptions = {
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||||
|
autoSelectFamily: true,
|
||||||
|
...(agentFamily ? { family: agentFamily } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpAgentOptions = {
|
||||||
|
maxCachedSessions: 0,
|
||||||
|
autoSelectFamily: true,
|
||||||
|
...(agentFamily ? { family: agentFamily } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||||
|
@ -491,6 +508,7 @@ class Monitor extends BeanModel {
|
||||||
if (proxy && proxy.active) {
|
if (proxy && proxy.active) {
|
||||||
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
|
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
|
||||||
httpsAgentOptions: httpsAgentOptions,
|
httpsAgentOptions: httpsAgentOptions,
|
||||||
|
httpAgentOptions: httpAgentOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
options.proxy = false;
|
options.proxy = false;
|
||||||
|
@ -499,6 +517,10 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!options.httpAgent) {
|
||||||
|
options.httpAgent = new http.Agent(httpAgentOptions);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.httpsAgent) {
|
if (!options.httpsAgent) {
|
||||||
let jar = new CookieJar();
|
let jar = new CookieJar();
|
||||||
let httpsCookieAgentOptions = {
|
let httpsCookieAgentOptions = {
|
||||||
|
|
|
@ -120,8 +120,8 @@ class StatusPage extends BeanModel {
|
||||||
|
|
||||||
const head = $("head");
|
const head = $("head");
|
||||||
|
|
||||||
if (statusPage.googleAnalyticsTagId) {
|
if (statusPage.google_analytics_tag_id) {
|
||||||
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId);
|
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id);
|
||||||
head.append($(escapedGoogleAnalyticsScript));
|
head.append($(escapedGoogleAnalyticsScript));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,9 @@ function NtlmClient(credentials, AxiosConfig) {
|
||||||
switch (_b.label) {
|
switch (_b.label) {
|
||||||
case 0:
|
case 0:
|
||||||
error = err.response;
|
error = err.response;
|
||||||
|
// The header may look like this: `Negotiate, NTLM, Basic realm="itsahiddenrealm.example.net"`Add commentMore actions
|
||||||
|
// so extract the 'NTLM' part first
|
||||||
|
const ntlmheader = error.headers['www-authenticate'].split(',').find(_ => _.match(/ *NTLM/))?.trim() || '';
|
||||||
if (!(error && error.status === 401
|
if (!(error && error.status === 401
|
||||||
&& error.headers['www-authenticate']
|
&& error.headers['www-authenticate']
|
||||||
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
|
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
|
||||||
|
@ -96,12 +99,12 @@ function NtlmClient(credentials, AxiosConfig) {
|
||||||
// include the Negotiate option when responding with the T2 message
|
// include the Negotiate option when responding with the T2 message
|
||||||
// There is nore we could do to ensure we are processing correctly,
|
// There is nore we could do to ensure we are processing correctly,
|
||||||
// but this is the easiest option for now
|
// but this is the easiest option for now
|
||||||
if (error.headers['www-authenticate'].length < 50) {
|
if (ntlmheader.length < 50) {
|
||||||
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
|
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
|
||||||
error.config.headers["Authorization"] = t1Msg;
|
error.config.headers["Authorization"] = t1Msg;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
|
t2Msg = ntlm.decodeType2Message((ntlmheader.match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
|
||||||
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
|
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
|
||||||
error.config.headers["X-retry"] = "false";
|
error.config.headers["X-retry"] = "false";
|
||||||
error.config.headers["Authorization"] = t3Msg;
|
error.config.headers["Authorization"] = t3Msg;
|
||||||
|
|
36
server/monitor-types/manual.js
Normal file
36
server/monitor-types/manual.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const { MonitorType } = require("./monitor-type");
|
||||||
|
const { UP, DOWN, PENDING } = require("../../src/util");
|
||||||
|
|
||||||
|
class ManualMonitorType extends MonitorType {
|
||||||
|
name = "Manual";
|
||||||
|
type = "manual";
|
||||||
|
description = "A monitor that allows manual control of the status";
|
||||||
|
supportsConditions = false;
|
||||||
|
conditionVariables = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async check(monitor, heartbeat) {
|
||||||
|
if (monitor.manual_status !== null) {
|
||||||
|
heartbeat.status = monitor.manual_status;
|
||||||
|
switch (monitor.manual_status) {
|
||||||
|
case UP:
|
||||||
|
heartbeat.msg = "Up";
|
||||||
|
break;
|
||||||
|
case DOWN:
|
||||||
|
heartbeat.msg = "Down";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
heartbeat.msg = "Pending";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
heartbeat.status = PENDING;
|
||||||
|
heartbeat.msg = "Manual monitoring - No status set";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ManualMonitorType
|
||||||
|
};
|
|
@ -41,8 +41,8 @@ class Ntfy extends NotificationProvider {
|
||||||
if (heartbeatJSON.status === DOWN) {
|
if (heartbeatJSON.status === DOWN) {
|
||||||
tags = [ "red_circle" ];
|
tags = [ "red_circle" ];
|
||||||
status = "Down";
|
status = "Down";
|
||||||
// if priority is not 5, increase priority for down alerts
|
// defaults to max(priority + 1, 5)
|
||||||
priority = priority === 5 ? priority : priority + 1;
|
priority = notification.ntfyPriorityDown || (priority === 5 ? priority : priority + 1);
|
||||||
} else if (heartbeatJSON["status"] === UP) {
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
tags = [ "green_circle" ];
|
tags = [ "green_circle" ];
|
||||||
status = "Up";
|
status = "Up";
|
||||||
|
|
|
@ -2,6 +2,7 @@ const PrometheusClient = require("prom-client");
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
const commonLabels = [
|
const commonLabels = [
|
||||||
|
"monitor_id",
|
||||||
"monitor_name",
|
"monitor_name",
|
||||||
"monitor_type",
|
"monitor_type",
|
||||||
"monitor_url",
|
"monitor_url",
|
||||||
|
@ -40,6 +41,7 @@ class Prometheus {
|
||||||
*/
|
*/
|
||||||
constructor(monitor) {
|
constructor(monitor) {
|
||||||
this.monitorLabelValues = {
|
this.monitorLabelValues = {
|
||||||
|
monitor_id: monitor.id,
|
||||||
monitor_name: monitor.name,
|
monitor_name: monitor.name,
|
||||||
monitor_type: monitor.type,
|
monitor_type: monitor.type,
|
||||||
monitor_url: monitor.url,
|
monitor_url: monitor.url,
|
||||||
|
|
|
@ -792,6 +792,7 @@ let needSetup = false;
|
||||||
bean.url = monitor.url;
|
bean.url = monitor.url;
|
||||||
bean.method = monitor.method;
|
bean.method = monitor.method;
|
||||||
bean.body = monitor.body;
|
bean.body = monitor.body;
|
||||||
|
bean.ipFamily = monitor.ipFamily;
|
||||||
bean.headers = monitor.headers;
|
bean.headers = monitor.headers;
|
||||||
bean.basic_auth_user = monitor.basic_auth_user;
|
bean.basic_auth_user = monitor.basic_auth_user;
|
||||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||||
|
@ -875,6 +876,7 @@ let needSetup = false;
|
||||||
bean.rabbitmqUsername = monitor.rabbitmqUsername;
|
bean.rabbitmqUsername = monitor.rabbitmqUsername;
|
||||||
bean.rabbitmqPassword = monitor.rabbitmqPassword;
|
bean.rabbitmqPassword = monitor.rabbitmqPassword;
|
||||||
bean.conditions = JSON.stringify(monitor.conditions);
|
bean.conditions = JSON.stringify(monitor.conditions);
|
||||||
|
bean.manual_status = monitor.manual_status;
|
||||||
|
|
||||||
// ping advanced options
|
// ping advanced options
|
||||||
bean.ping_numeric = monitor.ping_numeric;
|
bean.ping_numeric = monitor.ping_numeric;
|
||||||
|
|
|
@ -118,6 +118,7 @@ class UptimeKumaServer {
|
||||||
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
|
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
|
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
|
||||||
|
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
|
||||||
|
|
||||||
// Allow all CORS origins (polling) in development
|
// Allow all CORS origins (polling) in development
|
||||||
let cors = undefined;
|
let cors = undefined;
|
||||||
|
@ -558,4 +559,5 @@ const { GroupMonitorType } = require("./monitor-types/group");
|
||||||
const { SNMPMonitorType } = require("./monitor-types/snmp");
|
const { SNMPMonitorType } = require("./monitor-types/snmp");
|
||||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||||
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
|
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
|
||||||
|
const { ManualMonitorType } = require("./monitor-types/manual");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div v-if="selectedTags.length > 0" class="mb-2 p-1">
|
<div v-if="selectedTags.length > 0" class="mb-2 p-1">
|
||||||
<tag
|
<tag
|
||||||
v-for="item in selectedTags"
|
v-for="item in selectedTags"
|
||||||
:key="item.id"
|
:key="`${item.tag_id || item.id}-${item.value || ''}`"
|
||||||
:item="item"
|
:item="item"
|
||||||
:remove="deleteTag"
|
:remove="deleteTag"
|
||||||
/>
|
/>
|
||||||
|
@ -20,10 +20,20 @@
|
||||||
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
|
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="modal" class="modal fade" tabindex="-1">
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<h4 v-if="stagedForBatchAdd.length > 0">{{ $t("Add Tags") }}</h4>
|
||||||
|
<div v-if="stagedForBatchAdd.length > 0" class="mb-3 staging-area" style="max-height: 150px; overflow-y: auto;">
|
||||||
|
<Tag
|
||||||
|
v-for="stagedTag in stagedForBatchAdd"
|
||||||
|
:key="stagedTag.keyForList"
|
||||||
|
:item="mapStagedTagToDisplayItem(stagedTag)"
|
||||||
|
:remove="() => unstageTag(stagedTag)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<vue-multiselect
|
<vue-multiselect
|
||||||
v-model="newDraftTag.select"
|
v-model="newDraftTag.select"
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
|
@ -58,14 +68,11 @@
|
||||||
<div class="w-50 pe-2">
|
<div class="w-50 pe-2">
|
||||||
<input
|
<input
|
||||||
v-model="newDraftTag.name" class="form-control"
|
v-model="newDraftTag.name" class="form-control"
|
||||||
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
:class="{'is-invalid': validateDraftTag.invalid && (validateDraftTag.messageKey === 'tagNameColorRequired' || validateDraftTag.messageKey === 'tagNameExists')}"
|
||||||
:placeholder="$t('Name')"
|
:placeholder="$t('Name')"
|
||||||
data-testid="tag-name-input"
|
data-testid="tag-name-input"
|
||||||
@keydown.enter.prevent="onEnter"
|
@keydown.enter.prevent="onEnter"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback">
|
|
||||||
{{ $t("Tag with this name already exist.") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-50 ps-2">
|
<div class="w-50 ps-2">
|
||||||
<vue-multiselect
|
<vue-multiselect
|
||||||
|
@ -104,27 +111,24 @@
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<input
|
<input
|
||||||
v-model="newDraftTag.value" class="form-control"
|
v-model="newDraftTag.value" class="form-control"
|
||||||
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
:class="{'is-invalid': validateDraftTag.invalid && validateDraftTag.messageKey === 'tagAlreadyOnMonitor'}"
|
||||||
:placeholder="$t('value (optional)')"
|
:placeholder="$t('value (optional)')"
|
||||||
data-testid="tag-value-input"
|
data-testid="tag-value-input"
|
||||||
@keydown.enter.prevent="onEnter"
|
@keydown.enter.prevent="onEnter"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback">
|
|
||||||
{{ $t("Tag with this value already exist.") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
|
||||||
<button
|
<div v-if="validateDraftTag.invalid && validateDraftTag.messageKey" class="form-text text-danger mb-2">
|
||||||
type="button"
|
{{ $t(validateDraftTag.messageKey, validateDraftTag.messageParams) }}
|
||||||
class="btn btn-secondary float-end"
|
|
||||||
:disabled="processing || validateDraftTag.invalid"
|
|
||||||
data-testid="tag-submit-button"
|
|
||||||
@click.stop="addDraftTag"
|
|
||||||
>
|
|
||||||
{{ $t("Add") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click.stop="clearStagingAndCloseModal">{{ $t("Cancel") }}</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary me-2" :disabled="processing || validateDraftTag.invalid" @click.stop="stageCurrentTag">
|
||||||
|
{{ $t("Add Another Tag") }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" :disabled="processing || (stagedForBatchAdd.length === 0 && validateDraftTag.invalid)" data-testid="add-tags-final-button" @click.stop="confirmAndCommitStagedTags">{{ $t("Done") }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -176,71 +180,146 @@ export default {
|
||||||
newTags: [],
|
newTags: [],
|
||||||
/** @type {Tag[]} */
|
/** @type {Tag[]} */
|
||||||
deleteTags: [],
|
deleteTags: [],
|
||||||
|
/**
|
||||||
|
* @type {Array<object>} Holds tag objects staged for addition.
|
||||||
|
* Each object: { name, color, value, isNewSystemTag, systemTagId, keyForList }
|
||||||
|
*/
|
||||||
|
stagedForBatchAdd: [],
|
||||||
newDraftTag: {
|
newDraftTag: {
|
||||||
name: null,
|
name: null,
|
||||||
select: null,
|
select: null,
|
||||||
color: null,
|
color: null,
|
||||||
value: "",
|
value: "",
|
||||||
invalid: true,
|
|
||||||
nameInvalid: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tagOptions() {
|
tagOptions() {
|
||||||
const tagOptions = this.existingTags;
|
const tagOptions = [ ...this.existingTags ]; // Create a copy
|
||||||
|
|
||||||
|
// Add tags from newTags
|
||||||
for (const tag of this.newTags) {
|
for (const tag of this.newTags) {
|
||||||
if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) {
|
if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) {
|
||||||
tagOptions.push(tag);
|
tagOptions.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add newly created system tags from staging area
|
||||||
|
for (const stagedTag of this.stagedForBatchAdd) {
|
||||||
|
if (stagedTag.isNewSystemTag) {
|
||||||
|
// Check if this system tag is already in the options
|
||||||
|
if (!tagOptions.find(t => t.name === stagedTag.name && t.color === stagedTag.color)) {
|
||||||
|
// Create a tag option object for the dropdown
|
||||||
|
tagOptions.push({
|
||||||
|
id: null, // Will be assigned when actually created
|
||||||
|
name: stagedTag.name,
|
||||||
|
color: stagedTag.color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tagOptions;
|
return tagOptions;
|
||||||
},
|
},
|
||||||
selectedTags() {
|
selectedTags() {
|
||||||
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.tag_id === tag.tag_id));
|
// Helper function to normalize tag values for comparison
|
||||||
|
const normalizeValue = (value) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String(value).trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get tag ID from different structures
|
||||||
|
const getTagId = (tag) => tag.tag_id || tag.id;
|
||||||
|
|
||||||
|
return this.preSelectedTags.concat(this.newTags).filter(tag =>
|
||||||
|
!this.deleteTags.find(monitorTag => {
|
||||||
|
const tagIdMatch = getTagId(monitorTag) === getTagId(tag);
|
||||||
|
const valueMatch = normalizeValue(monitorTag.value) === normalizeValue(tag.value);
|
||||||
|
return tagIdMatch && valueMatch;
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @returns {boolean} True if more new system tags can be staged, false otherwise.
|
||||||
|
*/
|
||||||
|
canStageMoreNewSystemTags() {
|
||||||
|
return true; // Always allow adding more tags, no limit
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Provides the color options for the tag color selector.
|
||||||
|
* @returns {Array<object>} Array of color options.
|
||||||
|
*/
|
||||||
colorOptions() {
|
colorOptions() {
|
||||||
return colorOptions(this);
|
return colorOptions(this);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Validates the current draft tag based on several conditions.
|
||||||
|
* @returns {{invalid: boolean, messageKey: string|null, messageParams: object|null}} Object indicating validity, and a message key/params if invalid.
|
||||||
|
*/
|
||||||
validateDraftTag() {
|
validateDraftTag() {
|
||||||
let nameInvalid = false;
|
// If defining a new system tag (newDraftTag.select == null)
|
||||||
let valueInvalid = false;
|
if (this.newDraftTag.select == null) {
|
||||||
let invalid = true;
|
if (!this.newDraftTag.name || this.newDraftTag.name.trim() === "" || !this.newDraftTag.color) {
|
||||||
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value)) {
|
// Keep button disabled, but don't show the explicit message for this case
|
||||||
// Undo removing a Tag
|
return {
|
||||||
nameInvalid = false;
|
invalid: true,
|
||||||
valueInvalid = false;
|
messageKey: null,
|
||||||
invalid = false;
|
messageParams: null,
|
||||||
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0 && this.newDraftTag.select == null) {
|
};
|
||||||
// Try to create new tag with existing name
|
}
|
||||||
nameInvalid = true;
|
if (this.tagOptions.find(opt => opt.name.toLowerCase() === this.newDraftTag.name.trim().toLowerCase())) {
|
||||||
invalid = true;
|
return {
|
||||||
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
invalid: true,
|
||||||
tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value
|
messageKey: "tagNameExists",
|
||||||
) || (
|
messageParams: null,
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For any tag definition (new or existing system tag + value)
|
||||||
|
const draftTagName = this.newDraftTag.select ? this.newDraftTag.select.name : this.newDraftTag.name.trim();
|
||||||
|
const draftTagValue = this.newDraftTag.value ? this.newDraftTag.value.trim() : ""; // Treat null/undefined value as empty string for comparison
|
||||||
|
|
||||||
|
// Check if (name + value) combination already exists in this.stagedForBatchAdd
|
||||||
|
if (this.stagedForBatchAdd.find(staged => staged.name === draftTagName && staged.value === draftTagValue)) {
|
||||||
|
return {
|
||||||
|
invalid: true,
|
||||||
|
messageKey: "tagAlreadyStaged",
|
||||||
|
messageParams: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if (name + value) combination already exists in this.selectedTags (final list on monitor)
|
||||||
|
// AND it's NOT an "undo delete"
|
||||||
|
const isUndoDelete = this.deleteTags.find(dTag =>
|
||||||
|
dTag.tag_id === (this.newDraftTag.select ? this.newDraftTag.select.id : null) &&
|
||||||
|
dTag.value === draftTagValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUndoDelete && this.selectedTags.find(sTag => sTag.name === draftTagName && sTag.value === draftTagValue)) {
|
||||||
|
return {
|
||||||
|
invalid: true,
|
||||||
|
messageKey: "tagAlreadyOnMonitor",
|
||||||
|
messageParams: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// If an existing tag is selected at this point, it has passed all relevant checks
|
||||||
|
if (this.newDraftTag.select != null) {
|
||||||
|
return {
|
||||||
|
invalid: false,
|
||||||
|
messageKey: null,
|
||||||
|
messageParams: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a new tag definition, and it passed its specific checks, it's valid.
|
||||||
|
// (This also serves as a final default to valid if other logic paths were missed, though ideally covered above)
|
||||||
return {
|
return {
|
||||||
invalid,
|
invalid: false,
|
||||||
nameInvalid,
|
messageKey: null,
|
||||||
valueInvalid,
|
messageParams: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -257,6 +336,9 @@ export default {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
|
this.stagedForBatchAdd = [];
|
||||||
|
this.clearDraftTag();
|
||||||
|
this.getExistingTags();
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -300,37 +382,6 @@ export default {
|
||||||
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Add a draft tag
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Remove a draft tag
|
* Remove a draft tag
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
@ -341,10 +392,8 @@ export default {
|
||||||
select: null,
|
select: null,
|
||||||
color: null,
|
color: null,
|
||||||
value: "",
|
value: "",
|
||||||
invalid: true,
|
// invalid: true, // Initial validation will be handled by computed prop
|
||||||
nameInvalid: false,
|
|
||||||
};
|
};
|
||||||
this.modal.hide();
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Add a tag asynchronously
|
* Add a tag asynchronously
|
||||||
|
@ -386,7 +435,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
onEnter() {
|
onEnter() {
|
||||||
if (!this.validateDraftTag.invalid) {
|
if (!this.validateDraftTag.invalid) {
|
||||||
this.addDraftTag();
|
this.stageCurrentTag();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -475,7 +524,119 @@ export default {
|
||||||
console.warn("Modal hide failed:", e);
|
console.warn("Modal hide failed:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
this.stagedForBatchAdd = [];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Stages the current draft tag for batch addition.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
stageCurrentTag() {
|
||||||
|
if (this.validateDraftTag.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNew = this.newDraftTag.select == null;
|
||||||
|
const name = isNew ? this.newDraftTag.name.trim() : this.newDraftTag.select.name;
|
||||||
|
const color = isNew ? this.newDraftTag.color.color : this.newDraftTag.select.color;
|
||||||
|
const value = this.newDraftTag.value ? this.newDraftTag.value.trim() : "";
|
||||||
|
|
||||||
|
const stagedTagObject = {
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
value: value,
|
||||||
|
isNewSystemTag: isNew,
|
||||||
|
systemTagId: isNew ? null : this.newDraftTag.select.id,
|
||||||
|
keyForList: `staged-${Date.now()}-${Math.random().toString(36).substring(2, 15)}` // Unique key
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stagedForBatchAdd.push(stagedTagObject);
|
||||||
|
this.clearDraftTag(); // Reset input fields for the next tag
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Removes a tag from the staged list.
|
||||||
|
* @param {object} tagToUnstage The tag object to remove from staging.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
unstageTag(tagToUnstage) {
|
||||||
|
this.stagedForBatchAdd = this.stagedForBatchAdd.filter(tag => tag.keyForList !== tagToUnstage.keyForList);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Maps a staged tag object to the structure expected by the Tag component.
|
||||||
|
* @param {object} stagedTag The staged tag object.
|
||||||
|
* @returns {object} Object with name, color, value for the Tag component.
|
||||||
|
*/
|
||||||
|
mapStagedTagToDisplayItem(stagedTag) {
|
||||||
|
return {
|
||||||
|
name: stagedTag.name,
|
||||||
|
color: stagedTag.color,
|
||||||
|
value: stagedTag.value,
|
||||||
|
// id: stagedTag.keyForList, // Pass keyForList as id for the Tag component if it expects an id for display/keying internally beyond v-for key
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Clears the staging list, draft inputs, and closes the modal.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
clearStagingAndCloseModal() {
|
||||||
|
this.stagedForBatchAdd = [];
|
||||||
|
this.clearDraftTag(); // Clears input fields
|
||||||
|
this.modal.hide();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Processes all staged tags, adds them to the monitor, and closes the modal.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
confirmAndCommitStagedTags() {
|
||||||
|
// Phase 1: If there's a currently valid newDraftTag that hasn't been staged yet,
|
||||||
|
// (e.g. user typed a full tag and directly clicked the footer "Add"), then stage it now.
|
||||||
|
// stageCurrentTag has its own check for validateDraftTag.invalid and will clear the draft.
|
||||||
|
if (!this.validateDraftTag.invalid) {
|
||||||
|
// Check if newDraftTag actually has content, to avoid staging an empty cleared draft.
|
||||||
|
// A valid draft implies it has content, but double-checking select or name is safer.
|
||||||
|
if (this.newDraftTag.select || (this.newDraftTag.name && this.newDraftTag.color)) {
|
||||||
|
this.stageCurrentTag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Process everything that is now in stagedForBatchAdd.
|
||||||
|
if (this.stagedForBatchAdd.length === 0) {
|
||||||
|
this.clearDraftTag(); // Ensure draft is clear even if nothing was committed
|
||||||
|
this.modal.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sTag of this.stagedForBatchAdd) {
|
||||||
|
let isAnUndo = false; // Flag to track if this was an undo
|
||||||
|
// Check if it's an "undo delete"
|
||||||
|
if (sTag.systemTagId) { // Only existing system tags can be an undo delete
|
||||||
|
const undoDeleteIndex = this.deleteTags.findIndex(
|
||||||
|
dTag => dTag.tag_id === sTag.systemTagId && dTag.value === sTag.value
|
||||||
|
);
|
||||||
|
if (undoDeleteIndex > -1) {
|
||||||
|
this.deleteTags.splice(undoDeleteIndex, 1);
|
||||||
|
isAnUndo = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add to newTags if it's not an "undo delete" operation.
|
||||||
|
// An "undo delete" means the tag is now considered active again from its previous state.
|
||||||
|
if (!isAnUndo) {
|
||||||
|
const tagObjectForNewTags = {
|
||||||
|
id: sTag.systemTagId, // This will be null for brand new system tags
|
||||||
|
color: sTag.color,
|
||||||
|
name: sTag.name,
|
||||||
|
value: sTag.value,
|
||||||
|
new: true, // As per plan, signals new to this monitor transaction
|
||||||
|
};
|
||||||
|
this.newTags.push(tagObjectForNewTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDraftTag should have been cleared if stageCurrentTag ran in Phase 1, or earlier.
|
||||||
|
// Call clearDraftTag again to be certain the form is reset before closing.
|
||||||
|
this.clearDraftTag();
|
||||||
|
this.modal.hide();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,13 +13,20 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
||||||
|
<label for="ntfy-priority-down" class="form-label">{{ $t("ntfyPriorityDown") }}</label>
|
||||||
|
<input id="ntfy-priority-down" v-model="$parent.notification.ntfyPriorityDown" type="number" class="form-control" required min="1" max="5" step="1">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p v-if="$parent.notification.ntfyPriority >= 5">
|
<p v-if="$parent.notification.ntfyPriority == $parent.notification.ntfyPriorityDown && $parent.notification.ntfyPriority >= 5">
|
||||||
{{ $t("ntfyPriorityHelptextAllEvents") }}
|
{{ $t("ntfyPriorityHelptextAllEvents") }}
|
||||||
</p>
|
</p>
|
||||||
|
<i18n-t v-else-if="$parent.notification.ntfyPriority > $parent.notification.ntfyPriorityDown" tag="p" keypath="ntfyPriorityHelptextPriorityHigherThanDown">
|
||||||
|
<code>DOWN</code>
|
||||||
|
<code>{{ $parent.notification.ntfyPriority }}</code>
|
||||||
|
<code>{{ $parent.notification.ntfyPriorityDown }}</code>
|
||||||
|
</i18n-t>
|
||||||
<i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown">
|
<i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown">
|
||||||
<code>DOWN</code>
|
<code>DOWN</code>
|
||||||
<code>{{ $parent.notification.ntfyPriority + 1 }}</code>
|
<code>{{ $parent.notification.ntfyPriorityDown }}</code>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,6 +76,11 @@ export default {
|
||||||
this.$parent.notification.ntfyPriority = 5;
|
this.$parent.notification.ntfyPriority = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setting down priority if it's undefined
|
||||||
|
if (typeof this.$parent.notification.ntfyPriorityDown === "undefined") {
|
||||||
|
this.$parent.notification.ntfyPriorityDown = 5;
|
||||||
|
}
|
||||||
|
|
||||||
// Handling notifications that added before 1.22.0
|
// Handling notifications that added before 1.22.0
|
||||||
if (typeof this.$parent.notification.ntfyAuthenticationMethod === "undefined") {
|
if (typeof this.$parent.notification.ntfyAuthenticationMethod === "undefined") {
|
||||||
if (!this.$parent.notification.ntfyusername) {
|
if (!this.$parent.notification.ntfyusername) {
|
||||||
|
|
|
@ -188,9 +188,13 @@
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Show URI",
|
||||||
"Tags": "Tags",
|
"Tags": "Tags",
|
||||||
"Add New Tag": "Add New Tag",
|
"Add New Tag": "Add New Tag",
|
||||||
|
"Add Tags": "Add Tags",
|
||||||
"Add New below or Select...": "Add New below or Select…",
|
"Add New below or Select...": "Add New below or Select…",
|
||||||
"Tag with this name already exist.": "Tag with this name already exists.",
|
"Tag with this name already exist.": "Tag with this name already exists.",
|
||||||
"Tag with this value already exist.": "Tag with this value already exists.",
|
"Tag with this value already exist.": "Tag with this value already exists.",
|
||||||
|
"tagAlreadyOnMonitor": "This tag (name and value) is already on the monitor or pending addition.",
|
||||||
|
"tagAlreadyStaged": "This tag (name and value) is already staged for this batch.",
|
||||||
|
"tagNameExists": "A system tag with this name already exists. Select it from the list or use a different name.",
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"value (optional)": "value (optional)",
|
"value (optional)": "value (optional)",
|
||||||
"Gray": "Gray",
|
"Gray": "Gray",
|
||||||
|
@ -841,6 +845,8 @@
|
||||||
"ntfyAuthenticationMethod": "Authentication Method",
|
"ntfyAuthenticationMethod": "Authentication Method",
|
||||||
"ntfyPriorityHelptextAllEvents": "All events are sent with the maximum priority",
|
"ntfyPriorityHelptextAllEvents": "All events are sent with the maximum priority",
|
||||||
"ntfyPriorityHelptextAllExceptDown": "All events are sent with this priority, except {0}-events, which have a priority of {1}",
|
"ntfyPriorityHelptextAllExceptDown": "All events are sent with this priority, except {0}-events, which have a priority of {1}",
|
||||||
|
"ntfyPriorityHelptextPriorityHigherThanDown": "Regular priority should be higher than {0} priority. Priority {1} is higher than {0} priority {2}",
|
||||||
|
"ntfyPriorityDown": "Priority for DOWN-events",
|
||||||
"ntfyUsernameAndPassword": "Username and Password",
|
"ntfyUsernameAndPassword": "Username and Password",
|
||||||
"twilioAccountSID": "Account SID",
|
"twilioAccountSID": "Account SID",
|
||||||
"twilioApiKey": "Api Key (optional)",
|
"twilioApiKey": "Api Key (optional)",
|
||||||
|
@ -1109,5 +1115,13 @@
|
||||||
"Phone numbers": "Phone numbers",
|
"Phone numbers": "Phone numbers",
|
||||||
"Sender name": "Sender name",
|
"Sender name": "Sender name",
|
||||||
"smsplanetNeedToApproveName": "Needs to be approved in the client panel",
|
"smsplanetNeedToApproveName": "Needs to be approved in the client panel",
|
||||||
"Disable URL in Notification": "Disable URL in Notification"
|
"Disable URL in Notification": "Disable URL in Notification",
|
||||||
|
"Ip Family": "IP Family",
|
||||||
|
"ipFamilyDescriptionAutoSelect": "Uses the {happyEyeballs} for determining the IP family.",
|
||||||
|
"Happy Eyeballs algorithm": "Happy Eyeballs algorithm",
|
||||||
|
"Add Another Tag": "Add Another Tag",
|
||||||
|
"Staged Tags for Batch Add": "Staged Tags for Batch Add",
|
||||||
|
"Clear Form": "Clear Form",
|
||||||
|
"pause": "Pause",
|
||||||
|
"Manual": "Manual"
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,12 +223,12 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="mb-2">{{ $t("startDateTime") }}</div>
|
<div class="mb-2">{{ $t("startDateTime") }}</div>
|
||||||
<input v-model="maintenance.dateRange[0]" type="datetime-local" class="form-control" :required="maintenance.strategy === 'single'">
|
<input v-model="maintenance.dateRange[0]" type="datetime-local" max="9999-12-31T23:59" class="form-control" :required="maintenance.strategy === 'single'">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="mb-2">{{ $t("endDateTime") }}</div>
|
<div class="mb-2">{{ $t("endDateTime") }}</div>
|
||||||
<input v-model="maintenance.dateRange[1]" type="datetime-local" class="form-control" :required="maintenance.strategy === 'single'">
|
<input v-model="maintenance.dateRange[1]" type="datetime-local" max="9999-12-31T23:59" class="form-control" :required="maintenance.strategy === 'single'">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -55,6 +55,9 @@
|
||||||
<option value="push">
|
<option value="push">
|
||||||
Push
|
Push
|
||||||
</option>
|
</option>
|
||||||
|
<option value="manual">
|
||||||
|
{{ $t("Manual") }}
|
||||||
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
||||||
<optgroup :label="$t('Specific Monitor Type')">
|
<optgroup :label="$t('Specific Monitor Type')">
|
||||||
|
@ -115,6 +118,18 @@
|
||||||
<input id="name" v-model="monitor.name" type="text" class="form-control" data-testid="friendly-name-input" :placeholder="defaultFriendlyName">
|
<input id="name" v-model="monitor.name" type="text" class="form-control" data-testid="friendly-name-input" :placeholder="defaultFriendlyName">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Status switcher -->
|
||||||
|
<div v-if="monitor.type === 'manual'" class="mb-3">
|
||||||
|
<div class="btn-group w-100 mb-3">
|
||||||
|
<button class="btn btn-success" @click="monitor.manual_status = 1">
|
||||||
|
<i class="fas fa-check"></i> {{ $t("Up") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" @click="monitor.manual_status = 0">
|
||||||
|
<i class="fas fa-times"></i> {{ $t("Down") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- URL -->
|
<!-- URL -->
|
||||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
|
||||||
<label for="url" class="form-label">{{ $t("URL") }}</label>
|
<label for="url" class="form-label">{{ $t("URL") }}</label>
|
||||||
|
@ -745,6 +760,20 @@
|
||||||
{{ $t("acceptedStatusCodesDescription") }}
|
{{ $t("acceptedStatusCodesDescription") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ipFamily" class="form-label">{{ $t("Ip Family") }}</label>
|
||||||
|
<select id="ipFamily" v-model="monitor.ipFamily" class="form-select">
|
||||||
|
<option :value="null">{{ $t("auto-select") }}</option>
|
||||||
|
<option value="ipv4">IPv4</option>
|
||||||
|
<option value="ipv6">IPv6</option>
|
||||||
|
</select>
|
||||||
|
<i18n-t v-if="monitor.ipFamily == null" keypath="ipFamilyDescriptionAutoSelect" tag="div" class="form-text">
|
||||||
|
<template #happyEyeballs>
|
||||||
|
<a href="https://en.wikipedia.org/wiki/Happy_Eyeballs" target="_blank">{{ $t("Happy Eyeballs algorithm") }}</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Parent Monitor -->
|
<!-- Parent Monitor -->
|
||||||
|
@ -1129,6 +1158,7 @@ const monitorDefaults = {
|
||||||
parent: null,
|
parent: null,
|
||||||
url: "https://",
|
url: "https://",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
ipFamily: null,
|
||||||
interval: 60,
|
interval: 60,
|
||||||
retryInterval: 60,
|
retryInterval: 60,
|
||||||
resendInterval: 0,
|
resendInterval: 0,
|
||||||
|
|
|
@ -8,10 +8,14 @@ test.describe("Status Page", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create and edit", async ({ page }, testInfo) => {
|
test("create and edit", async ({ page }, testInfo) => {
|
||||||
|
test.setTimeout(60000); // Keep the timeout increase for stability
|
||||||
|
|
||||||
// Monitor
|
// Monitor
|
||||||
const monitorName = "Monitor for Status Page";
|
const monitorName = "Monitor for Status Page";
|
||||||
const tagName = "Client";
|
const tagName = "Client";
|
||||||
const tagValue = "Acme Inc";
|
const tagValue = "Acme Inc";
|
||||||
|
const tagName2 = "Project"; // Add second tag name
|
||||||
|
const tagValue2 = "Phoenix"; // Add second tag value
|
||||||
const monitorUrl = "https://www.example.com/status";
|
const monitorUrl = "https://www.example.com/status";
|
||||||
const monitorCustomUrl = "https://www.example.com";
|
const monitorCustomUrl = "https://www.example.com";
|
||||||
|
|
||||||
|
@ -33,12 +37,26 @@ test.describe("Status Page", () => {
|
||||||
await page.getByTestId("monitor-type-select").selectOption("http");
|
await page.getByTestId("monitor-type-select").selectOption("http");
|
||||||
await page.getByTestId("friendly-name-input").fill(monitorName);
|
await page.getByTestId("friendly-name-input").fill(monitorName);
|
||||||
await page.getByTestId("url-input").fill(monitorUrl);
|
await page.getByTestId("url-input").fill(monitorUrl);
|
||||||
|
|
||||||
|
// Modified tag section to add multiple tags
|
||||||
await page.getByTestId("add-tag-button").click();
|
await page.getByTestId("add-tag-button").click();
|
||||||
await page.getByTestId("tag-name-input").fill(tagName);
|
await page.getByTestId("tag-name-input").fill(tagName);
|
||||||
await page.getByTestId("tag-value-input").fill(tagValue);
|
await page.getByTestId("tag-value-input").fill(tagValue);
|
||||||
await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component
|
await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component
|
||||||
await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click();
|
await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click();
|
||||||
await page.getByTestId("tag-submit-button").click();
|
|
||||||
|
// Add another tag instead of submitting directly
|
||||||
|
await page.getByRole("button", { name: "Add Another Tag" }).click();
|
||||||
|
|
||||||
|
// Add second tag
|
||||||
|
await page.getByTestId("tag-name-input").fill(tagName2);
|
||||||
|
await page.getByTestId("tag-value-input").fill(tagValue2);
|
||||||
|
await page.getByTestId("tag-color-select").click();
|
||||||
|
await page.getByTestId("tag-color-select").getByRole("option", { name: "Blue" }).click();
|
||||||
|
|
||||||
|
// Submit both tags
|
||||||
|
await page.getByTestId("add-tags-final-button").click();
|
||||||
|
|
||||||
await page.getByTestId("save-button").click();
|
await page.getByTestId("save-button").click();
|
||||||
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
||||||
|
|
||||||
|
@ -61,8 +79,6 @@ test.describe("Status Page", () => {
|
||||||
await page.getByTestId("show-certificate-expiry-checkbox").uncheck();
|
await page.getByTestId("show-certificate-expiry-checkbox").uncheck();
|
||||||
await page.getByTestId("google-analytics-input").fill(googleAnalyticsId);
|
await page.getByTestId("google-analytics-input").fill(googleAnalyticsId);
|
||||||
await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism
|
await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism
|
||||||
await expect(page.getByTestId("description-editable")).toHaveText(descriptionText);
|
|
||||||
await expect(page.getByTestId("custom-footer-editable")).toHaveText(footerText);
|
|
||||||
|
|
||||||
// Add an incident
|
// Add an incident
|
||||||
await page.getByTestId("create-incident-button").click();
|
await page.getByTestId("create-incident-button").click();
|
||||||
|
@ -98,9 +114,7 @@ test.describe("Status Page", () => {
|
||||||
await expect(page.getByTestId("incident")).toHaveCount(1);
|
await expect(page.getByTestId("incident")).toHaveCount(1);
|
||||||
await expect(page.getByTestId("incident-title")).toContainText(incidentTitle);
|
await expect(page.getByTestId("incident-title")).toContainText(incidentTitle);
|
||||||
await expect(page.getByTestId("incident-content")).toContainText(incidentContent);
|
await expect(page.getByTestId("incident-content")).toContainText(incidentContent);
|
||||||
await expect(page.getByTestId("description")).toContainText(descriptionText);
|
|
||||||
await expect(page.getByTestId("group-name")).toContainText(groupName);
|
await expect(page.getByTestId("group-name")).toContainText(groupName);
|
||||||
await expect(page.getByTestId("footer-text")).toContainText(footerText);
|
|
||||||
await expect(page.getByTestId("powered-by")).toHaveCount(0);
|
await expect(page.getByTestId("powered-by")).toHaveCount(0);
|
||||||
|
|
||||||
await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl);
|
await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl);
|
||||||
|
@ -111,6 +125,11 @@ test.describe("Status Page", () => {
|
||||||
expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10);
|
expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10);
|
||||||
|
|
||||||
await expect(page.locator("body")).toHaveClass(theme);
|
await expect(page.locator("body")).toHaveClass(theme);
|
||||||
|
|
||||||
|
// Add Google Analytics ID to head and verify
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
return document.head.innerHTML.includes("https://www.googletagmanager.com/gtag/js?id=");
|
||||||
|
}, { timeout: 5000 });
|
||||||
expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId);
|
expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId);
|
||||||
|
|
||||||
const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor);
|
const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor);
|
||||||
|
@ -129,7 +148,10 @@ test.describe("Status Page", () => {
|
||||||
|
|
||||||
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
|
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
|
||||||
await expect(page.getByTestId("powered-by")).toContainText("Powered by");
|
await expect(page.getByTestId("powered-by")).toContainText("Powered by");
|
||||||
await expect(page.getByTestId("monitor-tag")).toContainText(tagValue);
|
|
||||||
|
// Modified tag verification to check both tags
|
||||||
|
await expect(page.getByTestId("monitor-tag").filter({ hasText: tagValue })).toBeVisible();
|
||||||
|
await expect(page.getByTestId("monitor-tag").filter({ hasText: tagValue2 })).toBeVisible();
|
||||||
|
|
||||||
await screenshot(testInfo, page);
|
await screenshot(testInfo, page);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue