uptime-kuma/src/pages/EditMonitor.vue
vishalsabhaya d0067a0a12
improve page load performance of large amount urls ()
Co-authored-by: vishal sabhaya <vishals@vebuin.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-06 03:36:54 +02:00

1917 lines
100 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit">
<div class="shadow-box shadow-box-with-fixed-bottom-bar">
<div class="row">
<div class="col-md-6">
<h2 class="mb-2">{{ $t("General") }}</h2>
<div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<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") }}
</option>
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="snmp">
SNMP
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
<option value="json-query">
HTTP(s) - {{ $t("Json Query") }}
</option>
<option value="grpc-keyword">
gRPC(s) - {{ $t("Keyword") }}
</option>
<option value="dns">
DNS
</option>
<option value="docker">
{{ $t("Docker Container") }}
</option>
<option value="real-browser">
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
</option>
</optgroup>
<optgroup :label="$t('Passive Monitor Type')">
<option value="push">
Push
</option>
</optgroup>
<optgroup :label="$t('Specific Monitor Type')">
<option value="steam">
{{ $t("Steam Game Server") }}
</option>
<option value="gamedig">
GameDig
</option>
<option value="mqtt">
MQTT
</option>
<option value="kafka-producer">
Kafka Producer
</option>
<option value="sqlserver">
Microsoft SQL Server
</option>
<option value="postgres">
PostgreSQL
</option>
<option value="mysql">
MySQL/MariaDB
</option>
<option value="mongodb">
MongoDB
</option>
<option value="radius">
Radius
</option>
<option value="redis">
Redis
</option>
<option v-if="!$root.info.isContainer" value="tailscale-ping">
Tailscale Ping
</option>
</optgroup>
</select>
</div>
<div v-if="monitor.type === 'tailscale-ping'" class="alert alert-warning" role="alert">
{{ $t("tailscalePingWarning") }}
</div>
<!-- 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 data-testid="friendly-name-input">
</div>
<!-- URL -->
<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>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required data-testid="url-input">
</div>
<!-- gRPC URL -->
<div v-if="monitor.type === 'grpc-keyword' " class="my-3">
<label for="grpc-url" class="form-label">{{ $t("URL") }}</label>
<input id="grpc-url" v-model="monitor.grpcUrl" type="text" class="form-control" required>
</div>
<!-- Push URL -->
<div v-if="monitor.type === 'push' " class="my-3">
<label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
<CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" />
<div class="form-text">
{{ $t("needPushEvery", [monitor.interval]) }}<br />
{{ $t("pushOptionalParams", ["status, msg, ping"]) }}
</div>
<button class="btn btn-primary" type="button" @click="resetToken">
{{ $t("Reset Token") }}
</button>
</div>
<!-- Keyword -->
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3">
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text">
{{ $t("keywordDescription") }}
</div>
</div>
<!-- Invert keyword -->
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3 form-check">
<input id="invert-keyword" v-model="monitor.invertKeyword" class="form-check-input" type="checkbox">
<label class="form-check-label" for="invert-keyword">
{{ $t("Invert Keyword") }}
</label>
<div class="form-text">
{{ $t("invertKeywordDescription") }}
</div>
</div>
<!-- Remote Browser -->
<div v-if="monitor.type === 'real-browser'" class="my-3">
<!-- Toggle -->
<div class="my-3 form-check">
<input id="toggle" v-model="remoteBrowsersToggle" class="form-check-input" type="checkbox">
<label class="form-check-label" for="toggle">
{{ $t("useRemoteBrowser") }}
</label>
<div class="form-text">
{{ $t("remoteBrowserToggle") }}
</div>
</div>
<div v-if="remoteBrowsersToggle">
<label for="remote-browser" class="form-label">{{ $t("Remote Browser") }}</label>
<ActionSelect
v-model="monitor.remote_browser"
:options="remoteBrowsersOptions"
icon="plus"
:action="() => $refs.remoteBrowserDialog.show()"
/>
</div>
</div>
<!-- Game -->
<!-- GameDig only -->
<div v-if="monitor.type === 'gamedig'" class="my-3">
<label for="game" class="form-label"> {{ $t("Game") }} </label>
<select id="game" v-model="monitor.game" class="form-select" required>
<option v-for="game in gameList" :key="game.keys[0]" :value="game.keys[0]">
{{ game.pretty }}
</option>
</select>
</div>
<template v-if="monitor.type === 'kafka-producer'">
<!-- Kafka Brokers List -->
<div class="my-3">
<label for="kafkaProducerBrokers" class="form-label">{{ $t("Kafka Brokers") }}</label>
<VueMultiselect
id="kafkaProducerBrokers"
v-model="monitor.kafkaProducerBrokers"
:multiple="true"
:options="[]"
:placeholder="$t('Enter the list of brokers')"
:tag-placeholder="$t('Press Enter to add broker')"
:max-height="500"
:taggable="true"
:show-no-options="false"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="false"
:preselect-first="false"
@tag="addKafkaProducerBroker"
></VueMultiselect>
</div>
<!-- Kafka Topic Name -->
<div class="my-3">
<label for="kafkaProducerTopic" class="form-label">{{ $t("Kafka Topic Name") }}</label>
<input id="kafkaProducerTopic" v-model="monitor.kafkaProducerTopic" type="text" class="form-control" required>
</div>
<!-- Kafka Producer Message -->
<div class="my-3">
<label for="kafkaProducerMessage" class="form-label">{{ $t("Kafka Producer Message") }}</label>
<input id="kafkaProducerMessage" v-model="monitor.kafkaProducerMessage" type="text" class="form-control" required>
</div>
<!-- Kafka SSL -->
<div class="my-3 form-check">
<input id="kafkaProducerSsl" v-model="monitor.kafkaProducerSsl" class="form-check-input" type="checkbox">
<label class="form-check-label" for="kafkaProducerSsl">
{{ $t("Enable Kafka SSL") }}
</label>
</div>
<!-- Kafka SSL -->
<div class="my-3 form-check">
<input id="kafkaProducerAllowAutoTopicCreation" v-model="monitor.kafkaProducerAllowAutoTopicCreation" class="form-check-input" type="checkbox">
<label class="form-check-label" for="kafkaProducerAllowAutoTopicCreation">
{{ $t("Enable Kafka Producer Auto Topic Creation") }}
</label>
</div>
</template>
<!-- Hostname -->
<!-- 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
data-testid="hostname-input"
>
</div>
<!-- Port -->
<!-- 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=">">&gt;</option>
<option value=">=">&gt;=</option>
<option value="<">&lt;</option>
<option value="<=">&lt;=</option>
<option value="!=">&#33;=</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'">
<div class="my-3">
<label for="dns_resolve_server" class="form-label">{{ $t("Resolver Server") }}</label>
<input id="dns_resolve_server" v-model="monitor.dns_resolve_server" type="text" class="form-control" :pattern="ipRegex" required>
<div class="form-text">
{{ $t("resolverserverDescription") }}
</div>
</div>
<!-- Port -->
<div 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 class="form-text">
{{ $t("dnsPortDescription") }}
</div>
</div>
<div class="my-3">
<label for="dns_resolve_type" class="form-label">{{ $t("Resource Record Type") }}</label>
<!-- :allow-empty="false" is not working, set a default value instead https://github.com/shentao/vue-multiselect/issues/336 -->
<VueMultiselect
id="dns_resolve_type"
v-model="monitor.dns_resolve_type"
:options="dnsresolvetypeOptions"
:multiple="false"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="false"
:placeholder="$t('Pick a RR-Type...')"
:preselect-first="false"
:max-height="500"
:taggable="false"
data-testid="resolve-type-select"
></VueMultiselect>
<div class="form-text">
{{ $t("rrtypeDescription") }}
</div>
</div>
</template>
<!-- Docker Container Name / ID -->
<!-- For Docker Type -->
<div v-if="monitor.type === 'docker'" class="my-3">
<label for="docker_container" class="form-label">{{ $t("Container Name / ID") }}</label>
<input id="docker_container" v-model="monitor.docker_container" type="text" class="form-control" required>
</div>
<!-- Docker Host -->
<!-- For Docker Type -->
<div v-if="monitor.type === 'docker'" class="my-3">
<div class="mb-3">
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
<ActionSelect
id="docker-host"
v-model="monitor.docker_host"
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
:options="dockerHostOptionsList"
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
:icon="'plus'"
:action="() => $refs.dockerHostDialog.show()"
:required="true"
/>
</div>
</div>
<!-- MQTT -->
<!-- For MQTT Type -->
<template v-if="monitor.type === 'mqtt'">
<div class="my-3">
<label for="mqttUsername" class="form-label">MQTT {{ $t("Username") }}</label>
<input id="mqttUsername" v-model="monitor.mqttUsername" type="text" class="form-control">
</div>
<div class="my-3">
<label for="mqttPassword" class="form-label">MQTT {{ $t("Password") }}</label>
<input id="mqttPassword" v-model="monitor.mqttPassword" type="password" class="form-control">
</div>
<div class="my-3">
<label for="mqttTopic" class="form-label">MQTT {{ $t("Topic") }}</label>
<input id="mqttTopic" v-model="monitor.mqttTopic" type="text" class="form-control" required>
<div class="form-text">
{{ $t("topicExplanation") }}
</div>
</div>
<div class="my-3">
<label for="mqttCheckType" class="form-label">MQTT {{ $t("Check Type") }}</label>
<select id="mqttCheckType" v-model="monitor.mqttCheckType" class="form-select" required>
<option value="keyword">{{ $t("Keyword") }}</option>
<option value="json-query">{{ $t("Json Query") }}</option>
</select>
</div>
<div v-if="monitor.mqttCheckType === 'keyword'" class="my-3">
<label for="mqttSuccessKeyword" class="form-label">MQTT {{ $t("successKeyword") }}</label>
<input id="mqttSuccessKeyword" v-model="monitor.mqttSuccessMessage" type="text" class="form-control">
<div class="form-text">
{{ $t("successKeywordExplanation") }}
</div>
</div>
<!-- Json Query -->
<div v-if="monitor.mqttCheckType === '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>
</template>
<template v-if="monitor.type === 'radius'">
<div class="my-3">
<label for="radius_username" class="form-label">Radius {{ $t("Username") }}</label>
<input id="radius_username" v-model="monitor.radiusUsername" type="text" class="form-control" required />
</div>
<div class="my-3">
<label for="radius_password" class="form-label">Radius {{ $t("Password") }}</label>
<input id="radius_password" v-model="monitor.radiusPassword" type="password" class="form-control" required />
</div>
<div class="my-3">
<label for="radius_secret" class="form-label">{{ $t("RadiusSecret") }}</label>
<input id="radius_secret" v-model="monitor.radiusSecret" type="password" class="form-control" required />
<div class="form-text"> {{ $t( "RadiusSecretDescription") }} </div>
</div>
<div class="my-3">
<label for="radius_called_station_id" class="form-label">{{ $t("RadiusCalledStationId") }}</label>
<input id="radius_called_station_id" v-model="monitor.radiusCalledStationId" type="text" class="form-control" required />
<div class="form-text"> {{ $t( "RadiusCalledStationIdDescription") }} </div>
</div>
<div class="my-3">
<label for="radius_calling_station_id" class="form-label">{{ $t("RadiusCallingStationId") }}</label>
<input id="radius_calling_station_id" v-model="monitor.radiusCallingStationId" type="text" class="form-control" required />
<div class="form-text"> {{ $t( "RadiusCallingStationIdDescription") }} </div>
</div>
</template>
<!-- SQL Server / PostgreSQL / MySQL / Redis / MongoDB -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql' || monitor.type === 'redis' || monitor.type === 'mongodb'">
<div class="my-3">
<label for="connectionString" class="form-label">{{ $t("Connection String") }}</label>
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
</div>
</template>
<template v-if="monitor.type === 'mysql'">
<div class="my-3">
<label for="mysql-password" class="form-label">{{ $t("Password") }}</label>
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
<HiddenInput id="mysql-password" v-model="monitor.radiusPassword" autocomplete="false"></HiddenInput>
</div>
</template>
<!-- SQL Server / PostgreSQL / MySQL -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
<div class="my-3">
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'SELECT 1' ])"></textarea>
</div>
</template>
<!-- MongoDB -->
<template v-if="monitor.type === 'mongodb'">
<div class="my-3">
<label for="mongodbCommand" class="form-label">{{ $t("Command") }}</label>
<textarea id="mongodbCommand" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ '{ &quot;ping&quot;: 1 }' ])"></textarea>
<i18n-t tag="div" class="form-text" keypath="mongodbCommandDescription">
<template #documentation>
<a href="https://www.mongodb.com/docs/manual/reference/command/">{{ $t('documentationOf', ['MongoDB']) }}</a>
</template>
</i18n-t>
</div>
<div 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">
<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>
</div>
<div class="my-3">
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control">
</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>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval" @blur="finishUpdateInterval">
</div>
<div class="my-3">
<label for="maxRetries" class="form-label">{{ $t("Retries") }}</label>
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
{{ $t("retriesDescription") }}
</div>
</div>
<div class="my-3">
<label for="retry-interval" class="form-label">
{{ $t("Heartbeat Retry Interval") }}
<span>({{ $t("retryCheckEverySecond", [ monitor.retryInterval ]) }})</span>
</label>
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
</div>
<!-- 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>
<div class="my-3">
<label for="resend-interval" class="form-label">
{{ $t("Resend Notification if Down X times consecutively") }}
<span v-if="monitor.resendInterval > 0">({{ $t("resendEveryXTimes", [ monitor.resendInterval ]) }})</span>
<span v-else>({{ $t("resendDisabled") }})</span>
</label>
<input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1">
</div>
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox" :disabled="monitor.ignoreTls">
<label class="form-check-label" for="expiry-notification">
{{ $t("Certificate Expiry Notification") }}
</label>
<div class="form-text">
</div>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
{{ monitor.type === "redis" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
</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">
{{ $t("Upside Down Mode") }}
</label>
<div class="form-text">
{{ $t("upsideDownModeDescription") }}
</div>
</div>
<div v-if="monitor.type === 'gamedig'" class="my-3 form-check">
<input id="gamedig-guess-port" v-model="monitor.gamedigGivenPortOnly" :true-value="false" :false-value="true" class="form-check-input" type="checkbox">
<label class="form-check-label" for="gamedig-guess-port">
{{ $t("gamedigGuessPort") }}
</label>
<div class="form-text">
{{ $t("gamedigGuessPortDescription") }}
</div>
</div>
<!-- Ping packet size -->
<div v-if="monitor.type === 'ping'" class="my-3">
<label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label>
<input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" max="65500" step="1">
</div>
<!-- HTTP / Keyword only -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' ">
<div class="my-3">
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
{{ $t("maxRedirectDescription") }}
</div>
</div>
<div class="my-3">
<label for="acceptedStatusCodes" class="form-label">{{ $t("Accepted Status Codes") }}</label>
<VueMultiselect
id="acceptedStatusCodes"
v-model="monitor.accepted_statuscodes"
:options="acceptedStatusCodeOptions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
:placeholder="$t('Pick Accepted Status Codes...')"
:preselect-first="false"
:max-height="600"
:taggable="true"
></VueMultiselect>
<div class="form-text">
{{ $t("acceptedStatusCodesDescription") }}
</div>
</div>
</template>
<!-- Parent Monitor -->
<div class="my-3">
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
<ActionSelect
id="monitorGroupSelector"
v-model="monitor.parent"
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
:options="parentMonitorOptionsList"
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
:icon="'plus'"
:action="() => $refs.createGroupDialog.show()"
/>
</div>
<!-- Description -->
<div class="my-3">
<label for="description" class="form-label">{{ $t("Description") }}</label>
<input id="description" v-model="monitor.description" type="text" class="form-control">
</div>
<div class="my-3">
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
</div>
</div>
<div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<!-- Notifications -->
<h2 class="mb-2">{{ $t("Notifications") }}</h2>
<p v-if="$root.notificationList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</label>
<span v-if="notification.isDefault == true" class="badge bg-primary ms-2">{{ $t("Default") }}</span>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
{{ $t("Setup Notification") }}
</button>
<!-- Proxies -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'">
<h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2>
<p v-if="$root.proxyList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<div v-if="$root.proxyList.length > 0" class="form-check my-3">
<input id="proxy-disable" v-model="monitor.proxyId" :value="null" name="proxy" class="form-check-input" type="radio">
<label class="form-check-label" for="proxy-disable">{{ $t("No Proxy") }}</label>
</div>
<div v-for="proxy in $root.proxyList" :key="proxy.id" class="form-check my-3">
<input :id="`proxy-${proxy.id}`" v-model="monitor.proxyId" :value="proxy.id" name="proxy" class="form-check-input" type="radio">
<label class="form-check-label" :for="`proxy-${proxy.id}`">
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
</label>
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("default") }}</span>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.proxyDialog.show()">
{{ $t("Setup Proxy") }}
</button>
</div>
<!-- Kafka SASL Options -->
<!-- Kafka Producer only -->
<template v-if="monitor.type === 'kafka-producer'">
<h2 class="mt-5 mb-2">{{ $t("Kafka SASL Options") }}</h2>
<div class="my-3">
<label class="form-label" for="kafkaProducerSaslMechanism">
{{ $t("Mechanism") }}
</label>
<VueMultiselect
id="kafkaProducerSaslMechanism"
v-model="monitor.kafkaProducerSaslOptions.mechanism"
:options="kafkaSaslMechanismOptions"
:multiple="false"
:clear-on-select="false"
:preserve-search="false"
:placeholder="$t('Pick a SASL Mechanism...')"
:preselect-first="false"
:max-height="500"
:allow-empty="false"
:taggable="false"
></VueMultiselect>
</div>
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'None'">
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'aws'" class="my-3">
<label for="kafkaProducerSaslUsername" class="form-label">{{ $t("Username") }}</label>
<input id="kafkaProducerSaslUsername" v-model="monitor.kafkaProducerSaslOptions.username" type="text" autocomplete="kafkaProducerSaslUsername" class="form-control">
</div>
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'aws'" class="my-3">
<label for="kafkaProducerSaslPassword" class="form-label">{{ $t("Password") }}</label>
<input id="kafkaProducerSaslPassword" v-model="monitor.kafkaProducerSaslOptions.password" type="password" autocomplete="kafkaProducerSaslPassword" class="form-control">
</div>
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
<label for="kafkaProducerSaslAuthorizationIdentity" class="form-label">{{ $t("Authorization Identity") }}</label>
<input id="kafkaProducerSaslAuthorizationIdentity" v-model="monitor.kafkaProducerSaslOptions.authorizationIdentity" type="text" autocomplete="kafkaProducerSaslAuthorizationIdentity" class="form-control" required>
</div>
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
<label for="kafkaProducerSaslAccessKeyId" class="form-label">{{ $t("AccessKey Id") }}</label>
<input id="kafkaProducerSaslAccessKeyId" v-model="monitor.kafkaProducerSaslOptions.accessKeyId" type="text" autocomplete="kafkaProducerSaslAccessKeyId" class="form-control" required>
</div>
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
<label for="kafkaProducerSaslSecretAccessKey" class="form-label">{{ $t("Secret AccessKey") }}</label>
<input id="kafkaProducerSaslSecretAccessKey" v-model="monitor.kafkaProducerSaslOptions.secretAccessKey" type="password" autocomplete="kafkaProducerSaslSecretAccessKey" class="form-control" required>
</div>
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
<label for="kafkaProducerSaslSessionToken" class="form-label">{{ $t("Session Token") }}</label>
<input id="kafkaProducerSaslSessionToken" v-model="monitor.kafkaProducerSaslOptions.sessionToken" type="password" autocomplete="kafkaProducerSaslSessionToken" class="form-control">
</div>
</div>
</template>
<!-- HTTP Options -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' ">
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
<!-- Method -->
<div class="my-3">
<label for="method" class="form-label">{{ $t("Method") }}</label>
<select id="method" v-model="monitor.method" class="form-select">
<option value="GET">
GET
</option>
<option value="POST">
POST
</option>
<option value="PUT">
PUT
</option>
<option value="PATCH">
PATCH
</option>
<option value="DELETE">
DELETE
</option>
<option value="HEAD">
HEAD
</option>
<option value="OPTIONS">
OPTIONS
</option>
</select>
</div>
<!-- Encoding -->
<div class="my-3">
<label for="httpBodyEncoding" class="form-label">{{ $t("Body Encoding") }}</label>
<select id="httpBodyEncoding" v-model="monitor.httpBodyEncoding" class="form-select">
<option value="json">JSON</option>
<option value="form">x-www-form-urlencoded</option>
<option value="xml">XML</option>
</select>
</div>
<!-- Body -->
<div class="my-3">
<label for="body" class="form-label">{{ $t("Body") }}</label>
<textarea id="body" v-model="monitor.body" class="form-control" :placeholder="bodyPlaceholder"></textarea>
</div>
<!-- Headers -->
<div class="my-3">
<label for="headers" class="form-label">{{ $t("Headers") }}</label>
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
</div>
<!-- HTTP Auth -->
<h4 class="mt-5 mb-2">{{ $t("Authentication") }}</h4>
<!-- Method -->
<div class="my-3">
<label for="method" class="form-label">{{ $t("Method") }}</label>
<select id="method" v-model="monitor.authMethod" class="form-select">
<option :value="null">
{{ $t("None") }}
</option>
<option value="basic">
{{ $t("HTTP Basic Auth") }}
</option>
<option value="oauth2-cc">
{{ $t("OAuth2: Client Credentials") }}
</option>
<option value="ntlm">
NTLM
</option>
<option value="mtls">
mTLS
</option>
</select>
</div>
<template v-if="monitor.authMethod && monitor.authMethod !== null ">
<template v-if="monitor.authMethod === 'mtls' ">
<div class="my-3">
<label for="tls-cert" class="form-label">{{ $t("Cert") }}</label>
<textarea id="tls-cert" v-model="monitor.tlsCert" class="form-control" :placeholder="$t('Cert body')" required></textarea>
</div>
<div class="my-3">
<label for="tls-key" class="form-label">{{ $t("Key") }}</label>
<textarea id="tls-key" v-model="monitor.tlsKey" class="form-control" :placeholder="$t('Key body')" required></textarea>
</div>
<div class="my-3">
<label for="tls-ca" class="form-label">{{ $t("CA") }}</label>
<textarea id="tls-ca" v-model="monitor.tlsCa" class="form-control" :placeholder="$t('Server CA')"></textarea>
</div>
</template>
<template v-else-if="monitor.authMethod === 'oauth2-cc' ">
<div class="my-3">
<label for="oauth_auth_method" class="form-label">{{ $t("Authentication Method") }}</label>
<select id="oauth_auth_method" v-model="monitor.oauth_auth_method" class="form-select">
<option value="client_secret_basic">
{{ $t("Authorization Header") }}
</option>
<option value="client_secret_post">
{{ $t("Form Data Body") }}
</option>
</select>
</div>
<div class="my-3">
<label for="oauth_token_url" class="form-label">{{ $t("OAuth Token URL") }}</label>
<input id="oauth_token_url" v-model="monitor.oauth_token_url" type="text" class="form-control" :placeholder="$t('OAuth Token URL')" required>
</div>
<div class="my-3">
<label for="oauth_client_id" class="form-label">{{ $t("Client ID") }}</label>
<input id="oauth_client_id" v-model="monitor.oauth_client_id" type="text" class="form-control" :placeholder="$t('Client ID')" required>
</div>
<template v-if="monitor.oauth_auth_method === 'client_secret_post' || monitor.oauth_auth_method === 'client_secret_basic'">
<div class="my-3">
<label for="oauth_client_secret" class="form-label">{{ $t("Client Secret") }}</label>
<input id="oauth_client_secret" v-model="monitor.oauth_client_secret" type="password" class="form-control" :placeholder="$t('Client Secret')" required>
</div>
<div class="my-3">
<label for="oauth_scopes" class="form-label">{{ $t("OAuth Scope") }}</label>
<input id="oauth_scopes" v-model="monitor.oauth_scopes" type="text" class="form-control" :placeholder="$t('Optional: Space separated list of scopes')">
</div>
</template>
</template>
<template v-else>
<div class="my-3">
<label for="basicauth-user" class="form-label">{{ $t("Username") }}</label>
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')">
</div>
<div class="my-3">
<label for="basicauth-pass" class="form-label">{{ $t("Password") }}</label>
<input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
</div>
<template v-if="monitor.authMethod === 'ntlm' ">
<div class="my-3">
<label for="ntlm-domain" class="form-label">{{ $t("Domain") }}</label>
<input id="ntlm-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
</div>
<div class="my-3">
<label for="ntlm-workstation" class="form-label">{{ $t("Workstation") }}</label>
<input id="ntlm-workstation" v-model="monitor.authWorkstation" type="text" class="form-control" :placeholder="$t('Workstation')">
</div>
</template>
</template>
</template>
</template>
<!-- gRPC Options -->
<template v-if="monitor.type === 'grpc-keyword' ">
<!-- Proto service enable TLS -->
<h2 class="mt-5 mb-2">{{ $t("GRPC Options") }}</h2>
<div class="my-3 form-check">
<input id="grpc-enable-tls" v-model="monitor.grpcEnableTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="grpc-enable-tls">
{{ $t("Enable TLS") }}
</label>
<div class="form-text">
{{ $t("enableGRPCTls") }}
</div>
</div>
<!-- Proto service name data -->
<div class="my-3">
<label for="protobuf" class="form-label">{{ $t("Proto Service Name") }}</label>
<input id="name" v-model="monitor.grpcServiceName" type="text" class="form-control" :placeholder="protoServicePlaceholder" required>
</div>
<!-- Proto method data -->
<div class="my-3">
<label for="protobuf" class="form-label">{{ $t("Proto Method") }}</label>
<input id="name" v-model="monitor.grpcMethod" type="text" class="form-control" :placeholder="protoMethodPlaceholder" required>
<div class="form-text">
{{ $t("grpcMethodDescription") }}
</div>
</div>
<!-- Proto data -->
<div class="my-3">
<label for="protobuf" class="form-label">{{ $t("Proto Content") }}</label>
<textarea id="protobuf" v-model="monitor.grpcProtobuf" class="form-control" :placeholder="protoBufDataPlaceholder"></textarea>
</div>
<!-- Body -->
<div class="my-3">
<label for="body" class="form-label">{{ $t("Body") }}</label>
<textarea id="body" v-model="monitor.grpcBody" class="form-control" :placeholder="bodyPlaceholder"></textarea>
</div>
<!-- Metadata: temporary disable waiting for next PR allow to send gRPC with metadata -->
<template v-if="false">
<div class="my-3">
<label for="metadata" class="form-label">{{ $t("Metadata") }}</label>
<textarea id="metadata" v-model="monitor.grpcMetadata" class="form-control" :placeholder="headersPlaceholder"></textarea>
</div>
</template>
</template>
</div>
</div>
<div class="fixed-bottom-bar p-3">
<button
id="monitor-submit-btn"
class="btn btn-primary me-2"
type="submit"
:disabled="processing"
data-testid="save-button"
>
{{ $t("Save") }}
</button>
<button
v-if="monitor.type === 'http'"
id="monitor-debug-btn"
class="btn btn-outline-primary"
type="button"
:disabled="processing"
@click.stop="modal.show()"
>
{{ $t("Debug") }}
</button>
</div>
</div>
</form>
<NotificationDialog ref="notificationDialog" @added="addedNotification" />
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
<RemoteBrowserDialog ref="remoteBrowserDialog" />
</div>
</transition>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
<button id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0" type="button" @click.stop="copyToClipboard">
<font-awesome-icon icon="copy" />
</button>
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
<template #newiline>
<br>
</template>
<template #firewalls>
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
</template>
<template #dns_resolvers>
<a href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/" target="_blank">{{ $t('dns resolvers') }}</a>
</template>
<template #docker_networks>
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
</template>
</i18n-t>
<div v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
<template #newline>
<br>
</template>
<template #oauth2_bearer>
<code>--oauth2-bearer TOKEN</code>
</template>
</i18n-t>
</div>
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification";
import ActionSelect from "../components/ActionSelect.vue";
import CopyableInput from "../components/CopyableInput.vue";
import CreateGroupDialog from "../components/CreateGroupDialog.vue";
import NotificationDialog from "../components/NotificationDialog.vue";
import DockerHostDialog from "../components/DockerHostDialog.vue";
import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue";
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";
import { version } from "../../package.json";
const userAgent = `'Uptime-Kuma/${version}'`;
const toast = useToast();
const pushTokenLength = 32;
const monitorDefaults = {
type: "http",
name: "",
parent: null,
url: "https://",
method: "GET",
interval: 60,
retryInterval: 60,
resendInterval: 0,
maxretries: 0,
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
packetSize: 56,
expiryNotification: false,
maxredirects: 10,
accepted_statuscodes: [ "200-299" ],
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
docker_container: "",
docker_host: null,
proxyId: null,
mqttUsername: "",
mqttPassword: "",
mqttTopic: "",
mqttSuccessMessage: "",
mqttCheckType: "keyword",
authMethod: null,
oauth_auth_method: "client_secret_basic",
httpBodyEncoding: "json",
kafkaProducerBrokers: [],
kafkaProducerSaslOptions: {
mechanism: "None",
},
cacheBust: false,
kafkaProducerSsl: false,
kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true,
remote_browser: null,
conditions: []
};
export default {
components: {
HiddenInput,
ActionSelect,
ProxyDialog,
CopyableInput,
CreateGroupDialog,
NotificationDialog,
DockerHostDialog,
RemoteBrowserDialog,
TagsManager,
VueMultiselect,
EditMonitorConditions,
},
data() {
return {
modal: null,
minInterval: MIN_INTERVAL_SECOND,
maxInterval: MAX_INTERVAL_SECOND,
processing: false,
monitor: {
notificationIDList: {},
// Do not add default value here, please check init() method
},
acceptedStatusCodeOptions: [],
dnsresolvetypeOptions: [],
kafkaSaslMechanismOptions: [],
ipOrHostnameRegexPattern: hostNameRegexPattern(),
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true),
gameList: null,
connectionStringTemplates: {
"sqlserver": "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>",
"postgres": "postgres://username:password@host:port/database",
"mysql": "mysql://username:password@host:port/database",
"redis": "redis://user:password@host:port",
"mongodb": "mongodb://username:password@host:port/database",
},
draftGroupName: null,
remoteBrowsersEnabled: false,
};
},
computed: {
curlCommand() {
const command = [ "curl", "--verbose", "--head", "--request", this.monitor.method, "\\\n", "--user-agent", userAgent, "\\\n" ];
if (this.monitor.ignoreTls) {
command.push("--insecure", "\\\n");
}
if (this.monitor.headers) {
try {
// trying to parse the supplied data as json to trim whitespace
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
command.push("--header", `'${key}: ${value}'`, "\\\n");
}
} catch (e) {
command.push("--header", `'${this.monitor.headers}'`, "\\\n");
}
}
if (this.monitor.authMethod === "basic") {
command.push("--user", `${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}`, "--basic", "\\\n");
} else if (this.monitor.authmethod === "mtls") {
command.push("--cacert", `'${this.monitor.tlsCa}'`, "\\\n", "--key", `'${this.monitor.tlsKey}'`, "\\\n", "--cert", `'${this.monitor.tlsCert}'`, "\\\n");
} else if (this.monitor.authMethod === "ntlm") {
command.push("--user", `'${this.monitor.authDomain ? `${this.monitor.authDomain}/` : ""}${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}'`, "--ntlm", "\\\n");
}
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
let json = "";
try {
// trying to parse the supplied data as json to trim whitespace
json = JSON.stringify(JSON.parse(this.monitor.body));
} catch (e) {
json = this.monitor.body;
}
command.push("--header", "'Content-Type: application/json'", "\\\n", "--data", `'${json}'`, "\\\n");
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
command.push("--headers", "'Content-Type: application/xml'", "\\\n", "--data", `'${this.monitor.body}'`, "\\\n");
}
if (this.monitor.maxredirects) {
command.push("--location", "--max-redirs", this.monitor.maxredirects, "\\\n");
}
if (this.monitor.timeout) {
command.push("--max-time", this.monitor.timeout, "\\\n");
}
if (this.monitor.maxretries) {
command.push("--retry", this.monitor.maxretries, "\\\n");
}
command.push("--url", this.monitor.url);
return command.join(" ");
},
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)
if (! isDev) {
return this.ipRegexPattern;
}
return null;
},
pageName() {
let name = "Add New Monitor";
if (this.isClone) {
name = "Clone Monitor";
} else if (this.isEdit) {
name = "Edit";
}
return this.$t(name);
},
remoteBrowsersOptions() {
return this.$root.remoteBrowserList.map(browser => {
return {
label: browser.name,
value: browser.id,
};
});
},
remoteBrowsersToggle: {
get() {
return this.remoteBrowsersEnabled || this.monitor.remote_browser != null;
},
set(value) {
if (value) {
this.remoteBrowsersEnabled = true;
if (this.monitor.remote_browser == null && this.$root.remoteBrowserList.length > 0) {
// set a default remote browser if there is one. Otherwise, the user will have to select one manually.
this.monitor.remote_browser = this.$root.remoteBrowserList[0].id;
}
} else {
this.remoteBrowsersEnabled = false;
this.monitor.remote_browser = null;
}
}
},
isAdd() {
return this.$route.path === "/add";
},
isClone() {
return this.$route.path.startsWith("/clone");
},
isEdit() {
return this.$route.path.startsWith("/edit");
},
pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
},
protoServicePlaceholder() {
return this.$t("Example:", [ "Health" ]);
},
protoMethodPlaceholder() {
return this.$t("Example:", [ "check" ]);
},
protoBufDataPlaceholder() {
return this.$t("Example:", [ `
syntax = "proto3";
package grpc.health.v1;
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
}
ServingStatus status = 1;
}
` ]);
},
bodyPlaceholder() {
if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") {
return this.$t("Example:", [ `
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<Uptime>Kuma</Uptime>
</soap:Body>
</soap:Envelope>` ]);
}
if (this.monitor && this.monitor.httpBodyEncoding === "form") {
return this.$t("Example:", [ "key1=value1&key2=value2" ]);
}
return this.$t("Example:", [ `
{
"key": "value"
}` ]);
},
headersPlaceholder() {
return this.$t("Example:", [ `
{
"HeaderName": "HeaderValue"
}` ]);
},
currentGameObject() {
if (this.gameList) {
for (let game of this.gameList) {
if (game.keys[0] === this.monitor.game) {
return game;
}
}
}
return null;
},
// Filter result by active state, weight and alphabetical
// Only return groups which arent't itself and one of its decendants
sortedGroupMonitorList() {
let result = Object.values(this.$root.monitorList);
// 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)
);
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.pathName.localeCompare(m2.pathName);
});
return result;
},
/**
* Generates the parent monitor options list based on the sorted group monitor list and draft group name.
* @returns {Array} The parent monitor options list.
*/
parentMonitorOptionsList() {
let list = [];
if (this.sortedGroupMonitorList.length === 0 && this.draftGroupName == null) {
list = [
{
label: this.$t("noGroupMonitorMsg"),
value: null
}
];
} else {
list = [
{
label: this.$t("None"),
value: null
},
... this.sortedGroupMonitorList.map(monitor => {
return {
label: monitor.pathName,
value: monitor.id,
};
}),
];
}
if (this.draftGroupName != null) {
list = [{
label: this.draftGroupName,
value: -1,
}].concat(list);
}
return list;
},
dockerHostOptionsList() {
if (this.$root.dockerHostList && this.$root.dockerHostList.length > 0) {
return this.$root.dockerHostList.map((host) => {
return {
label: host.name,
value: host.id
};
});
} else {
return [{
label: this.$t("noDockerHostMsg"),
value: null,
}];
}
},
supportsConditions() {
return this.$root.monitorTypeList[this.monitor.type]?.supportsConditions || false;
},
conditionVariables() {
return this.$root.monitorTypeList[this.monitor.type]?.conditionVariables || [];
},
},
watch: {
"$root.proxyList"() {
if (this.isAdd) {
if (this.$root.proxyList && !this.monitor.proxyId) {
const proxy = this.$root.proxyList.find(proxy => proxy.default);
if (proxy) {
this.monitor.proxyId = proxy.id;
}
}
}
},
"$route.fullPath"() {
this.init();
},
"monitor.interval"(value, oldValue) {
// Link interval and retryInterval if they are the same value.
if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value;
}
},
"monitor.timeout"(value, oldValue) {
// keep timeout within 80% range
if (value && value !== oldValue) {
this.monitor.timeout = this.clampTimeout(value);
}
},
"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
// it's very unlikely to get a collision though (62^32 ~ 2.27265788 * 10^57 unique tokens)
this.monitor.pushToken = genSecret(pushTokenLength);
}
}
// Set default port for DNS if not already defined
if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") {
if (this.monitor.type === "dns") {
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) => {
if (res.ok) {
this.gameList = res.gameList;
} else {
this.$root.toastError(res.msg);
}
});
}
// Set default database connection string if empty or it is a template from another database monitor type
for (let monitorType in this.connectionStringTemplates) {
if (this.monitor.type === monitorType) {
let isTemplate = false;
for (let key in this.connectionStringTemplates) {
if (this.monitor.databaseConnectionString === this.connectionStringTemplates[key]) {
isTemplate = true;
break;
}
}
if (!this.monitor.databaseConnectionString || isTemplate) {
this.monitor.databaseConnectionString = this.connectionStringTemplates[monitorType];
}
break;
}
}
// Reset conditions since condition variables likely change:
if (oldType && newType !== oldType) {
this.monitor.conditions = [];
}
},
currentGameObject(newGameObject, previousGameObject) {
if (!this.monitor.port || (previousGameObject && previousGameObject.options.port === this.monitor.port)) {
this.monitor.port = newGameObject.options.port;
}
this.monitor.game = newGameObject.keys[0];
},
"monitor.ignoreTls"(newVal) {
if (newVal) {
this.monitor.expiryNotification = false;
}
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
this.init();
let acceptedStatusCodeOptions = [
"100-199",
"200-299",
"300-399",
"400-499",
"500-599",
];
let dnsresolvetypeOptions = [
"A",
"AAAA",
"CAA",
"CNAME",
"MX",
"NS",
"PTR",
"SOA",
"SRV",
"TXT",
];
let kafkaSaslMechanismOptions = [
"None",
"plain",
"scram-sha-256",
"scram-sha-512",
"aws",
];
for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString());
}
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
},
methods: {
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.curlCommand);
toast.success(this.$t("CopyToClipboardSuccess"));
} catch (err) {
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
}
},
/**
* Initialize the edit monitor form
* @returns {void}
*/
init() {
if (this.isAdd) {
this.monitor = {
...monitorDefaults
};
if (this.$root.proxyList && !this.monitor.proxyId) {
const proxy = this.$root.proxyList.find(proxy => proxy.default);
if (proxy) {
this.monitor.proxyId = proxy.id;
}
}
for (let i = 0; i < this.$root.notificationList.length; i++) {
if (this.$root.notificationList[i].isDefault === true) {
this.monitor.notificationIDList[this.$root.notificationList[i].id] = true;
}
}
} else if (this.isEdit || this.isClone) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
if (res.ok) {
if (this.isClone) {
// Reset push token for cloned monitors
if (res.monitor.type === "push") {
res.monitor.pushToken = undefined;
}
}
this.monitor = res.monitor;
if (this.isClone) {
/*
* Cloning a monitor will include properties that can not be posted to backend
* as they are not valid columns in the SQLite table.
*/
this.monitor.id = undefined; // Remove id when cloning as we want a new id
this.monitor.includeSensitiveData = undefined;
this.monitor.maintenance = undefined;
// group monitor fields
this.monitor.childrenIDs = undefined;
this.monitor.forceInactive = undefined;
this.monitor.path = undefined;
this.monitor.pathName = undefined;
this.monitor.screenshot = undefined;
this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]);
this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => {
return {
id: monitorTag.tag_id,
name: monitorTag.name,
color: monitorTag.color,
value: monitorTag.value,
new: true,
};
});
this.monitor.tags = undefined;
}
// Handling for monitors that are created before 1.7.0
if (this.monitor.retryInterval === 0) {
this.monitor.retryInterval = this.monitor.interval;
}
// Handling for monitors that are missing/zeroed timeout
if (!this.monitor.timeout) {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
}
} else {
this.$root.toastError(res.msg);
}
});
}
this.draftGroupName = null;
},
addKafkaProducerBroker(newBroker) {
this.monitor.kafkaProducerBrokers.push(newBroker);
},
/**
* Validate form input
* @returns {boolean} Is the form input valid?
*/
isInputValid() {
if (this.monitor.body && (!this.monitor.httpBodyEncoding || this.monitor.httpBodyEncoding === "json")) {
try {
JSON.parse(this.monitor.body);
} catch (err) {
toast.error(this.$t("BodyInvalidFormat") + err.message);
return false;
}
}
if (this.monitor.headers) {
try {
JSON.parse(this.monitor.headers);
} catch (err) {
toast.error(this.$t("HeadersInvalidFormat") + err.message);
return false;
}
}
if (this.monitor.type === "docker") {
if (this.monitor.docker_host == null) {
toast.error(this.$t("DockerHostRequired"));
return false;
}
}
return true;
},
resetToken() {
this.monitor.pushToken = genSecret(pushTokenLength);
},
/**
* Submit the form data for processing
* @returns {Promise<void>}
*/
async submit() {
this.processing = true;
if (!this.isInputValid()) {
this.processing = false;
return;
}
// Beautify the JSON format (only if httpBodyEncoding is not set or === json)
if (this.monitor.body && (!this.monitor.httpBodyEncoding || this.monitor.httpBodyEncoding === "json")) {
this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
}
const monitorTypesWithEncodingAllowed = [ "http", "keyword", "json-query" ];
if (this.monitor.type && !monitorTypesWithEncodingAllowed.includes(this.monitor.type)) {
this.monitor.httpBodyEncoding = null;
}
if (this.monitor.headers) {
this.monitor.headers = JSON.stringify(JSON.parse(this.monitor.headers), null, 4);
}
if (this.monitor.hostname) {
this.monitor.hostname = this.monitor.hostname.trim();
}
if (this.monitor.url) {
this.monitor.url = this.monitor.url.trim();
}
let createdNewParent = false;
if (this.draftGroupName && this.monitor.parent === -1) {
// Create Monitor with name of draft group
const res = await new Promise((resolve) => {
this.$root.add({
...monitorDefaults,
type: "group",
name: this.draftGroupName,
interval: this.monitor.interval,
active: false,
}, resolve);
});
if (res.ok) {
createdNewParent = true;
this.monitor.parent = res.monitorID;
} else {
this.$root.toastError(res.msg);
this.processing = false;
return;
}
}
if (this.isAdd || this.isClone) {
this.$root.add(this.monitor, async (res) => {
if (res.ok) {
await this.$refs.tagsManager.submit(res.monitorID);
// Start the new parent monitor after edit is done
if (createdNewParent) {
await this.startParentGroupMonitor();
}
this.processing = false;
this.$router.push("/dashboard/" + res.monitorID);
} else {
this.processing = false;
}
this.$root.toastRes(res);
});
} else {
await this.$refs.tagsManager.submit(this.monitor.id);
this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
this.processing = false;
this.$root.toastRes(res);
this.init();
// Start the new parent monitor after edit is done
if (createdNewParent) {
this.startParentGroupMonitor();
}
});
}
},
async startParentGroupMonitor() {
await sleep(2000);
await this.$root.getSocket().emit("resumeMonitor", this.monitor.parent, () => {});
},
/**
* Added a Notification Event
* Enable it if the notification is added in EditMonitor.vue
* @param {number} id ID of notification to add
* @returns {void}
*/
addedNotification(id) {
this.monitor.notificationIDList[id] = true;
},
/**
* Added a Proxy Event
* Enable it if the proxy is added in EditMonitor.vue
* @param {number} id ID of proxy to add
* @returns {void}
*/
addedProxy(id) {
this.monitor.proxyId = id;
},
/**
* Added a Docker Host Event
* Enable it if the Docker Host is added in EditMonitor.vue
* @param {number} id ID of docker host
* @returns {void}
*/
addedDockerHost(id) {
this.monitor.docker_host = id;
},
/**
* Adds a draft group.
* @param {string} draftGroupName The name of the draft group.
* @returns {void}
*/
addedDraftGroup(draftGroupName) {
this.draftGroupName = draftGroupName;
this.monitor.parent = -1;
},
// Clamp timeout
clampTimeout(timeout) {
// limit to 80% of interval, narrowly avoiding epsilon bug
const maxTimeout = ~~(this.monitor.interval * 8 ) / 10;
const clamped = Math.max(0, Math.min(timeout, maxTimeout));
// 0 will be treated as 80% of interval
return Number.isFinite(clamped) ? clamped : maxTimeout;
},
finishUpdateInterval() {
// Update timeout if it is greater than the clamp timeout
let clampedValue = this.clampTimeout(this.monitor.interval);
if (this.monitor.timeout > clampedValue) {
this.monitor.timeout = clampedValue;
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
textarea {
min-height: 200px;
}
#curl-debug {
font-family: monospace;
overflow: auto;
}
</style>