mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-18 04:04:41 -05:00
Merge pull request #1964 from minhhoangvn/feat/add-gRPC-protocol
Feat/add gRPC protocol
This commit is contained in:
commit
cc6d17d2e0
25
db/patch-grpc-monitor.sql
Normal file
25
db/patch-grpc-monitor.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_url VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_protobuf TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_body TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_metadata TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_method VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_service_name VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_enable_tls BOOLEAN default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
4140
package-lock.json
generated
4140
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -64,6 +64,7 @@
|
|||||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
|
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.7.0",
|
||||||
"@louislam/sqlite3": "15.1.2",
|
"@louislam/sqlite3": "15.1.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.27.0",
|
"axios": "~0.27.0",
|
||||||
@ -103,6 +104,7 @@
|
|||||||
"pg-connection-string": "~2.5.0",
|
"pg-connection-string": "~2.5.0",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"protobufjs": "~7.1.1",
|
||||||
"redbean-node": "0.1.4",
|
"redbean-node": "0.1.4",
|
||||||
"socket.io": "~4.5.3",
|
"socket.io": "~4.5.3",
|
||||||
"socket.io-client": "~4.5.3",
|
"socket.io-client": "~4.5.3",
|
||||||
|
@ -62,6 +62,7 @@ class Database {
|
|||||||
"patch-add-clickable-status-page-link.sql": true,
|
"patch-add-clickable-status-page-link.sql": true,
|
||||||
"patch-add-sqlserver-monitor.sql": true,
|
"patch-add-sqlserver-monitor.sql": true,
|
||||||
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
||||||
|
"patch-grpc-monitor.sql": true,
|
||||||
"patch-add-radius-monitor.sql": true,
|
"patch-add-radius-monitor.sql": true,
|
||||||
"patch-monitor-add-resend-interval.sql": true,
|
"patch-monitor-add-resend-interval.sql": true,
|
||||||
"patch-maintenance-table2.sql": true,
|
"patch-maintenance-table2.sql": true,
|
||||||
|
@ -3,7 +3,7 @@ const dayjs = require("dayjs");
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
@ -105,6 +105,11 @@ class Monitor extends BeanModel {
|
|||||||
authMethod: this.authMethod,
|
authMethod: this.authMethod,
|
||||||
authWorkstation: this.authWorkstation,
|
authWorkstation: this.authWorkstation,
|
||||||
authDomain: this.authDomain,
|
authDomain: this.authDomain,
|
||||||
|
grpcUrl: this.grpcUrl,
|
||||||
|
grpcProtobuf: this.grpcProtobuf,
|
||||||
|
grpcMethod: this.grpcMethod,
|
||||||
|
grpcServiceName: this.grpcServiceName,
|
||||||
|
grpcEnableTls: this.getGrpcEnableTls(),
|
||||||
radiusUsername: this.radiusUsername,
|
radiusUsername: this.radiusUsername,
|
||||||
radiusPassword: this.radiusPassword,
|
radiusPassword: this.radiusPassword,
|
||||||
radiusCalledStationId: this.radiusCalledStationId,
|
radiusCalledStationId: this.radiusCalledStationId,
|
||||||
@ -117,6 +122,8 @@ class Monitor extends BeanModel {
|
|||||||
...data,
|
...data,
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
body: this.body,
|
body: this.body,
|
||||||
|
grpcBody: this.grpcBody,
|
||||||
|
grpcMetadata: this.grpcMetadata,
|
||||||
basic_auth_user: this.basic_auth_user,
|
basic_auth_user: this.basic_auth_user,
|
||||||
basic_auth_pass: this.basic_auth_pass,
|
basic_auth_pass: this.basic_auth_pass,
|
||||||
pushToken: this.pushToken,
|
pushToken: this.pushToken,
|
||||||
@ -167,6 +174,14 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.upsideDown);
|
return Boolean(this.upsideDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
getGrpcEnableTls() {
|
||||||
|
return Boolean(this.grpcEnableTls);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get accepted status codes
|
* Get accepted status codes
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
@ -527,6 +542,37 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "grpc-keyword") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
const options = {
|
||||||
|
grpcUrl: this.grpcUrl,
|
||||||
|
grpcProtobufData: this.grpcProtobuf,
|
||||||
|
grpcServiceName: this.grpcServiceName,
|
||||||
|
grpcEnableTls: this.grpcEnableTls,
|
||||||
|
grpcMethod: this.grpcMethod,
|
||||||
|
grpcBody: this.grpcBody,
|
||||||
|
keyword: this.keyword
|
||||||
|
};
|
||||||
|
const response = await grpcQuery(options);
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
|
||||||
|
let responseData = response.data;
|
||||||
|
if (responseData.length > 50) {
|
||||||
|
responseData = response.substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
if (response.code !== 1) {
|
||||||
|
bean.status = DOWN;
|
||||||
|
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||||
|
} else {
|
||||||
|
if (response.data.toString().includes(this.keyword)) {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
|
||||||
|
} else {
|
||||||
|
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
|
||||||
|
bean.status = DOWN;
|
||||||
|
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (this.type === "postgres") {
|
} else if (this.type === "postgres") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
@ -706,6 +706,12 @@ let needSetup = false;
|
|||||||
bean.authMethod = monitor.authMethod;
|
bean.authMethod = monitor.authMethod;
|
||||||
bean.authWorkstation = monitor.authWorkstation;
|
bean.authWorkstation = monitor.authWorkstation;
|
||||||
bean.authDomain = monitor.authDomain;
|
bean.authDomain = monitor.authDomain;
|
||||||
|
bean.grpcUrl = monitor.grpcUrl;
|
||||||
|
bean.grpcProtobuf = monitor.grpcProtobuf;
|
||||||
|
bean.grpcMethod = monitor.grpcMethod;
|
||||||
|
bean.grpcBody = monitor.grpcBody;
|
||||||
|
bean.grpcMetadata = monitor.grpcMetadata;
|
||||||
|
bean.grpcEnableTls = monitor.grpcEnableTls;
|
||||||
bean.radiusUsername = monitor.radiusUsername;
|
bean.radiusUsername = monitor.radiusUsername;
|
||||||
bean.radiusPassword = monitor.radiusPassword;
|
bean.radiusPassword = monitor.radiusPassword;
|
||||||
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
||||||
|
@ -15,6 +15,8 @@ const { Client } = require("pg");
|
|||||||
const postgresConParse = require("pg-connection-string").parse;
|
const postgresConParse = require("pg-connection-string").parse;
|
||||||
const { NtlmClient } = require("axios-ntlm");
|
const { NtlmClient } = require("axios-ntlm");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
|
const grpc = require("@grpc/grpc-js");
|
||||||
|
const protojs = require("protobufjs");
|
||||||
const radiusClient = require("node-radius-client");
|
const radiusClient = require("node-radius-client");
|
||||||
const {
|
const {
|
||||||
dictionaries: {
|
dictionaries: {
|
||||||
@ -720,3 +722,51 @@ module.exports.timeObjectToUTC = (obj, timezone = undefined) => {
|
|||||||
module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
|
module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
|
||||||
return timeObjectConvertTimezone(obj, timezone, false);
|
return timeObjectConvertTimezone(obj, timezone, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create gRPC client stib
|
||||||
|
* @param {Object} options from gRPC client
|
||||||
|
*/
|
||||||
|
module.exports.grpcQuery = async (options) => {
|
||||||
|
const { grpcUrl, grpcProtobufData, grpcServiceName, grpcEnableTls, grpcMethod, grpcBody } = options;
|
||||||
|
const protocObject = protojs.parse(grpcProtobufData);
|
||||||
|
const protoServiceObject = protocObject.root.lookupService(grpcServiceName);
|
||||||
|
const Client = grpc.makeGenericClientConstructor({});
|
||||||
|
const credentials = grpcEnableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
|
||||||
|
const client = new Client(
|
||||||
|
grpcUrl,
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
const grpcService = protoServiceObject.create(function (method, requestData, cb) {
|
||||||
|
const fullServiceName = method.fullName;
|
||||||
|
const serviceFQDN = fullServiceName.split(".");
|
||||||
|
const serviceMethod = serviceFQDN.pop();
|
||||||
|
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
|
||||||
|
log.debug("monitor", `gRPC method ${serviceMethodClientImpl}`);
|
||||||
|
client.makeUnaryRequest(
|
||||||
|
serviceMethodClientImpl,
|
||||||
|
arg => arg,
|
||||||
|
arg => arg,
|
||||||
|
requestData,
|
||||||
|
cb);
|
||||||
|
}, false, false);
|
||||||
|
return new Promise((resolve, _) => {
|
||||||
|
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
|
||||||
|
const responseData = JSON.stringify(response);
|
||||||
|
if (err) {
|
||||||
|
return resolve({
|
||||||
|
code: err.code,
|
||||||
|
errorMessage: err.details,
|
||||||
|
data: ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.debug("monitor:", `gRPC response: ${response}`);
|
||||||
|
return resolve({
|
||||||
|
code: 1,
|
||||||
|
errorMessage: "",
|
||||||
|
data: responseData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -8,6 +8,8 @@ export default {
|
|||||||
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
||||||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||||
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||||
|
enableGRPCTls: "Allow to send gRPC request with TLS connection",
|
||||||
|
grpcMethodDescription: "Method name is convert to cammelCase format such as sayHello, check, etc.",
|
||||||
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
||||||
Maintenance: "Maintenance",
|
Maintenance: "Maintenance",
|
||||||
statusMaintenance: "Maintenance",
|
statusMaintenance: "Maintenance",
|
||||||
|
@ -24,6 +24,9 @@
|
|||||||
<option value="keyword">
|
<option value="keyword">
|
||||||
HTTP(s) - {{ $t("Keyword") }}
|
HTTP(s) - {{ $t("Keyword") }}
|
||||||
</option>
|
</option>
|
||||||
|
<option value="grpc-keyword">
|
||||||
|
gRPC(s) - {{ $t("Keyword") }}
|
||||||
|
</option>
|
||||||
<option value="dns">
|
<option value="dns">
|
||||||
DNS
|
DNS
|
||||||
</option>
|
</option>
|
||||||
@ -70,6 +73,12 @@
|
|||||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||||
</div>
|
</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="url" class="form-control" pattern="[^\:]+:[0-9]{5}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Push URL -->
|
<!-- Push URL -->
|
||||||
<div v-if="monitor.type === 'push' " class="my-3">
|
<div v-if="monitor.type === 'push' " class="my-3">
|
||||||
<label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
|
<label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
|
||||||
@ -81,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Keyword -->
|
<!-- Keyword -->
|
||||||
<div v-if="monitor.type === 'keyword' " class="my-3">
|
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' " class="my-3">
|
||||||
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
|
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
|
||||||
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
@ -313,7 +322,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTTP / Keyword only -->
|
<!-- HTTP / Keyword only -->
|
||||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
|
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'grpc-keyword' ">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
|
<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">
|
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||||
@ -491,6 +500,55 @@
|
|||||||
</template>
|
</template>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -569,6 +627,40 @@ export default {
|
|||||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
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() {
|
bodyPlaceholder() {
|
||||||
return this.$t("Example:", [ `
|
return this.$t("Example:", [ `
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user