Compare commits

..
Author SHA1 Message Date
tomsun28 abf947a6ff [improve] update git archive export ignore
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-03-24 23:03:00 +08:00
Yang Chen 311cb131fd [bugfix] fix bind Labels are not updated when the Alarm Severity switches (#3170) 2025-03-24 22:12:06 +08:00
tomsun28andaias00 21558e9b4f [bugfix] fix postgre mount error, use mariadb instead of mysql in compose (#3168)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <rokkki@163.com>
2025-03-24 15:12:56 +08:00
LL-LINandaias00 8d1f306433 [feature] implement labels-based monitors filtering in bulletin creation flow (#3161)
Co-authored-by: aias00 <rokkki@163.com>
2025-03-24 10:46:52 +08:00
tomsun28 e98af9527e [release] update release version 1.7.0 version and docs (#3165)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-03-23 10:39:00 +08:00
811a07553b [doc] Add alert integration japanese i18 (#3164)
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 14:16:28 +08:00
9fd58ced4a [feature] add twilio sms client support (#3159)
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 14:12:32 +08:00
LL-LINandtomsun28 a331c045ae [bugfix] retain sorting state after monitor list auto-refresh (#3156)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 11:48:34 +08:00
yunfan24andaias00 63d3d18428 [bugfix] Fix http header being incorrectly encoded. (#3108)
Co-authored-by: aias00 <rokkki@163.com>
2025-03-22 11:44:16 +08:00
Yang Chen cec8d8b396 [bugfix]: fix incomplete class documentation in AppServiceImpl (#3162) 2025-03-21 17:34:43 +08:00
9d68d16a78 [feature] Support TencentCloud alert source (#3149)
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-17 20:55:56 +08:00
Kerwin BryantandLogic 290474a715 Abstract Redundant Input Components into ConfigurableFieldComponent for Unified Management (#3152)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-03-17 18:50:00 +08:00
Yang Chenandyunfan24 0dbc7347b4 [feature] Supports sending messages to a specific Telegram group topic.(#3079) (#3143)
Co-authored-by: yunfan24 <yunfan24@outlook.com>
2025-03-17 14:56:02 +08:00
tomsun28 e2a2d5eaec [bugfix] fix nightly docker build github action (#3153) 2025-03-17 10:00:41 +08:00
tomsun28 356cca295d [webapp] fix less style file over max build error (#3151)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-03-16 21:58:45 +08:00
yunfan24 5e09c6d9ae [improve] Merge SMS configuration class. (#3148) 2025-03-16 21:27:07 +08:00
tomsun28andZhang Yuxuan 54caa84fc1 [bugfix] fix collect dispatch error (#3150)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Zhang Yuxuan <1373529784@qq.com>
2025-03-15 21:49:58 +08:00
116 changed files with 1713 additions and 953 deletions
+36
View File
@@ -2325,6 +2325,42 @@
"contributions": [
"code"
]
},
{
"login": "JuJinPark",
"name": "jujin",
"avatar_url": "https://avatars.githubusercontent.com/u/44892459?v=4",
"profile": "https://github.com/JuJinPark",
"contributions": [
"code"
]
},
{
"login": "LL-LIN",
"name": "LL-LIN",
"avatar_url": "https://avatars.githubusercontent.com/u/43002118?v=4",
"profile": "https://github.com/LL-LIN",
"contributions": [
"code"
]
},
{
"login": "bigcyy",
"name": "Yang Chen",
"avatar_url": "https://avatars.githubusercontent.com/u/73413979?v=4",
"profile": "https://bigcyy.github.io/",
"contributions": [
"code"
]
},
{
"login": "sarthakeash",
"name": "Sarthak Arora",
"avatar_url": "https://avatars.githubusercontent.com/u/74091160?v=4",
"profile": "https://github.com/sarthakeash",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+2
View File
@@ -17,7 +17,9 @@
/home export-ignore
/hip export-ignore
/template-marketplace export-ignore
/.github export-ignore
/.idea export-ignore
/.devcontainer export-ignore
.gitattributes export-ignore
.gitignore export-ignore
+2
View File
@@ -31,6 +31,8 @@ name: "CodeQL"
on:
schedule:
- cron: '21 13 * * 4'
push:
branches: [ action* ]
jobs:
analyze:
+1 -1
View File
@@ -19,7 +19,7 @@ name: DOC CI
on:
push:
branches: [ master, dev ]
branches: [ master, dev, action* ]
paths:
- 'home/**'
pull_request:
+2 -1
View File
@@ -17,7 +17,8 @@
name: 'HertzBeat Doc Pdf Builder'
on:
page_build
push:
branches: [ page_build* ]
jobs:
build:
+1 -1
View File
@@ -22,7 +22,7 @@ name: Frontend CI
on:
push:
branches: [ master, dev ]
branches: [ master, dev, action* ]
paths:
- 'web-app/**'
pull_request:
+1 -1
View File
@@ -22,7 +22,7 @@ name: License Checker
on:
push:
branches: [ master, dev ]
branches: [ master, dev, action* ]
pull_request:
branches: [ master, dev ]
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
run: |
mvn clean install
mvn clean package -Prelease -DskipTests
cd collector
cd hertzbeat-collector
mvn clean package -Pcluster -DskipTests
- uses: docker/setup-qemu-action@v3
+3
View File
@@ -13,6 +13,7 @@ mvnw.bat
package-lock.json
*.zip
jdk/
data/
### STS ###
.apt_generated
@@ -54,3 +55,5 @@ node_modules
application-dev.yml
application-mysql.yml
application-pg.yml
/package.json
/yarn.lock
+6
View File
@@ -510,6 +510,12 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+6
View File
@@ -509,6 +509,12 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+5
View File
@@ -73,6 +73,11 @@
"field": "successCode",
"type": 4,
"paramValue": "200, 201"
},
{
"field": "enableUrlEncoding",
"type": 1,
"paramValue": true
}
]
}
@@ -17,30 +17,39 @@
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Alibaba Cloud SMS configuration properties
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AlibabaSmsProperties {
/**
* Alibaba Cloud access key id
*/
@NotBlank(message = "AccessKeyId cannot be empty")
private String accessKeyId;
/**
* Alibaba Cloud access key secret
*/
@NotBlank(message = "AccessKeySecret cannot be empty")
private String accessKeySecret;
/**
* SMS signature
*/
@NotBlank(message = "SignName cannot be empty")
private String signName;
/**
* SMS template Code
*/
@NotBlank(message = "TemplateCode cannot be null")
private String templateCode;
}
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
@@ -27,16 +28,19 @@ public class AwsSmsProperties {
/**
* AWS Cloud access key id
*/
@NotBlank(message = "AccessKeyId cannot be empty")
private String accessKeyId;
/**
* AWS Cloud access key secret
*/
@NotBlank(message = "AccessKeySecret cannot be empty")
private String accessKeySecret;
/**
* AWS Cloud region
*/
@NotBlank(message = "region cannot be empty")
private String region;
}
@@ -17,7 +17,10 @@
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@@ -25,6 +28,8 @@ import org.springframework.stereotype.Component;
* SMS configuration
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "alerter.sms")
public class SmsConfig {
@@ -37,6 +42,7 @@ public class SmsConfig {
/**
* sms service provider
*/
@NotBlank(message = "Type cannot be empty")
private String type;
/**
@@ -58,6 +64,12 @@ public class SmsConfig {
* Aws configuration
*/
private AwsSmsProperties aws;
/**
* Twilio SMS configuration
*/
private TwilioSmsProperties twilio;
/**
* Smslocal SMS configuration
*/
@@ -18,12 +18,16 @@
package org.apache.hertzbeat.alert.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Smslocal SMS Properties
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SmslocalSmsProperties {
/**
* SmsLocal account api key
@@ -17,35 +17,45 @@
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Tencent Cloud SMS properties
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TencentSmsProperties {
/**
* Tencent cloud account secret id
*/
@NotBlank(message = "SecretId cannot be empty")
private String secretId;
/**
* Tencent cloud account secret key
*/
@NotBlank(message = "SecretKey cannot be empty")
private String secretKey;
/**
* SMS app id
*/
@NotBlank(message = "AppId cannot be null")
private String appId;
/**
* SMS signature
*/
@NotBlank(message = "SignName cannot be empty")
private String signName;
/**
* SMS template ID
*/
@NotBlank(message = "templateId cannot be null")
private String templateId;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.manager.pojo.dto;
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -23,25 +23,27 @@ import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Sms Sender configuration dto
* Twilio SMS configuration properties
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SmsNoticeSender {
@NotBlank(message = "Type cannot be empty")
private String type;
private SmsTencentConfig tencent;
private SmsAlibabaConfig alibaba;
@NoArgsConstructor
public class TwilioSmsProperties {
/**
* Twilio Account SID
*/
@NotBlank(message = "Account SID cannot be empty")
private String accountSid;
private SmsUniSmsConfig unisms;
/**
* Twilio Auth Token
*/
@NotBlank(message = "Auth Token cannot be empty")
private String authToken;
private SmslocalConfig smslocal;
private SmsAwsConfig aws;
private boolean enable = true;
}
/**
* Twilio Issued Phone Number
*/
@NotBlank(message = "Twilio Phone Number cannot be empty")
private String twilioPhoneNumber;
}
@@ -17,16 +17,22 @@
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* UniSMS properties
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UniSmsProperties {
/**
* UniSMS access key id
*/
@NotBlank(message = "accessKeyId cannot be empty")
private String accessKeyId;
/**
@@ -37,15 +43,18 @@ public class UniSmsProperties {
/**
* SMS signature
*/
@NotBlank(message = "signature cannot be null")
private String signature;
/**
* SMS template ID
*/
@NotBlank(message = "templateId cannot be null")
private String templateId;
/**
* Authentication mode: simple or hmac, default is simple
*/
@NotBlank(message = "authMode cannot be null")
private String authMode = "simple";
}
@@ -118,11 +118,11 @@ public class TencentCloudExternAlert {
/**
* Transaction alarm
*/
private static final String EVENT = "event";
public static final String EVENT = "event";
/**
* Indicator alarm
*/
private static final String METRIC = "metric";
public static final String METRIC = "metric";
}
@@ -48,6 +48,7 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
TelegramBotNotifyDTO notifyBody = TelegramBotNotifyDTO.builder()
.chatId(receiver.getTgUserId())
.text(renderContent(noticeTemplate, alert))
.messageThreadId(receiver.getTgMessageThreadId())
.disableWebPagePreview(true)
.build();
HttpHeaders headers = new HttpHeaders();
@@ -84,6 +85,8 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
private String text;
@JsonProperty("disable_web_page_preview")
private Boolean disableWebPagePreview;
@JsonProperty("message_thread_id")
private String messageThreadId;
}
@NoArgsConstructor
@@ -23,6 +23,7 @@ import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.UniSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AlibabaSmsClientImpl;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
@@ -35,6 +36,7 @@ import org.springframework.stereotype.Component;
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
import static org.apache.hertzbeat.common.constants.SmsConstants.AWS;
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO;
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
import static org.apache.hertzbeat.common.constants.SmsConstants.SMSLOCAL;
@@ -141,9 +143,12 @@ public class SmsClientFactory {
case AWS:
currentSmsClient = new AwsSmsClientImpl(smsConfig.getAws());
break;
case TWILIO:
currentSmsClient = new TwilioSmsClientImpl(smsConfig.getTwilio());
break;
default:
log.warn("[SmsClientFactory] Unsupported SMS provider type: {}", smsConfig.getType());
break;
}
}
}
}
@@ -18,14 +18,22 @@
package org.apache.hertzbeat.alert.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.TencentCloudExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Default external alarm service impl
*/
@@ -43,8 +51,7 @@ public class TencentExternAlertService implements ExternAlertService {
log.warn("parse extern alert content failed! content: {}", content);
return;
}
// todo convert TenCloudAlertReport to SingleAlert
SingleAlert alert = new SingleAlert();
SingleAlert alert = new TencentCloudAlertConverter().convert(report);
alarmCommonReduce.reduceAndSendAlarm(alert);
}
@@ -52,4 +59,169 @@ public class TencentExternAlertService implements ExternAlertService {
public String supportSource() {
return "tencent";
}
/**
* Converter: TencentCloud alert to SingleAlert
*/
public static class TencentCloudAlertConverter {
/**
* Metric alert content template
*/
private final String metricTemplate = "Tencent Cloud Metric Alert: {metricShowName} {calcType} {calcValue}{calcUnit}, Current: {currentValue}{unit}";
/**
* Event alert content template
*/
private final String eventTemplate = "Tencent Cloud Event Alert: {productShowName} - {eventShowName}";
/**
* Convert TencentCloud alert to SingleAlert
*/
public SingleAlert convert(TencentCloudExternAlert tencentAlert) {
// Building basic information
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
.status(convertStatus(tencentAlert.getAlarmStatus()))
.startAt(parseTime(tencentAlert.getFirstOccurTime()))
.activeAt(parseTime(tencentAlert.getFirstOccurTime()))
.triggerTimes(1);
// If it is a recovery state, set the end time.
if ("0".equals(tencentAlert.getAlarmStatus())) {
builder.endAt(parseTime(tencentAlert.getRecoverTime()));
}
// Build labels
Map<String, String> labels = new HashMap<>();
buildLabels(labels, tencentAlert);
// Build annotations
Map<String, String> annotations = new HashMap<>();
buildAnnotations(annotations, tencentAlert);
// Build content
String content = generateContent(tencentAlert);
return builder
.labels(labels)
.annotations(annotations)
.content(content)
.build();
}
private void buildLabels(Map<String, String> labels, TencentCloudExternAlert alert) {
labels.put("__source__", "tencent_cloud");
labels.put("alert_type", alert.getAlarmType());
if (TencentCloudExternAlert.METRIC.equals(alert.getAlarmType())) {
labels.put("metric_name", alert.getAlarmPolicyInfo().getConditions().getMetricName());
labels.put("namespace", alert.getAlarmObjInfo().getNamespace());
} else {
labels.put("event_name", alert.getAlarmPolicyInfo().getConditions().getEventName());
labels.put("product_name", alert.getAlarmPolicyInfo().getConditions().getProductName());
}
}
private void buildAnnotations(Map<String, String> annotations, TencentCloudExternAlert alert) {
TencentCloudExternAlert.AlarmPolicyInfo alarmPolicyInfo = alert.getAlarmPolicyInfo();
TencentCloudExternAlert.AlarmObjInfo alarmObjInfo = alert.getAlarmObjInfo();
TencentCloudExternAlert.Dimensions dimensions = alert.getAlarmObjInfo().getDimensions();
putIfNotNull(annotations, "policy_id", alarmPolicyInfo.getPolicyId());
putIfNotNull(annotations, "policy_type", alarmPolicyInfo.getPolicyType());
putIfNotNull(annotations, "policy_name", alarmPolicyInfo.getPolicyName());
putIfNotNull(annotations, "policy_type_cname", alarmPolicyInfo.getPolicyTypeCname());
putIfNotNull(annotations, "namespace", alarmObjInfo.getNamespace());
putIfNotNull(annotations, "region", alarmObjInfo.getRegion());
putIfNotNull(annotations, "app_id", alarmObjInfo.getAppId());
putIfNotNull(annotations, "uin", alarmObjInfo.getUin());
putIfNotNull(annotations, "instance_id", dimensions.getUnInstanceId());
putIfNotNull(annotations, "obj_id", dimensions.getObjId());
}
/**
* Generate alert content with template
*/
private String generateContent(TencentCloudExternAlert alert) {
if (TencentCloudExternAlert.METRIC.equals(alert.getAlarmType())) {
return generateMetricContent(alert);
} else {
return generateEventContent(alert);
}
}
/**
* Generate metric alert content
*/
private String generateMetricContent(TencentCloudExternAlert alert) {
TencentCloudExternAlert.Conditions conditions = alert.getAlarmPolicyInfo().getConditions();
Map<String, String> params = new HashMap<>();
params.put("metricShowName", conditions.getMetricShowName());
if (StringUtils.isNotEmpty(conditions.getCalcType())
&& StringUtils.isNotEmpty(conditions.getCalcValue())) {
params.put("calcType", conditions.getCalcType());
params.put("calcValue", conditions.getCalcValue());
params.put("calcUnit", Objects.toString(conditions.getCalcUnit(), ""));
} else {
params.put("calcType", "");
params.put("calcValue", "N/A");
params.put("calcUnit", "");
}
if (StringUtils.isNotEmpty(conditions.getCurrentValue())) {
params.put("currentValue", conditions.getCurrentValue());
params.put("unit", Objects.toString(conditions.getUnit(), ""));
} else {
params.put("currentValue", "N/A");
params.put("unit", "");
}
return replacePlaceholders(metricTemplate, params);
}
/**
* Generate event alert content
*/
private String generateEventContent(TencentCloudExternAlert alert) {
TencentCloudExternAlert.Conditions conditions = alert.getAlarmPolicyInfo().getConditions();
Map<String, String> params = new HashMap<>();
params.put("productShowName", conditions.getProductShowName());
params.put("eventShowName", conditions.getEventShowName());
return replacePlaceholders(eventTemplate, params);
}
/**
* Replace placeholders in template with actual values
*/
private String replacePlaceholders(String template, Map<String, String> params) {
String result = template;
for (Map.Entry<String, String> entry : params.entrySet()) {
result = result.replace("{" + entry.getKey() + "}", Objects.toString(entry.getValue(), "NULL"));
}
return result;
}
private String convertStatus(String alarmStatus) {
return "1".equals(alarmStatus) ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED;
}
private Long parseTime(String timeStr) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(timeStr).getTime();
} catch (ParseException e) {
log.error("Failed to parse time: {}", timeStr);
throw new IllegalArgumentException("Failed to parse time: " + timeStr, e);
}
}
private void putIfNotNull(Map<String, String> map, String key, String value){
if (StringUtils.isNotEmpty(value)){
map.put(key, value);
}
}
}
}
@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import java.net.URI;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO;
/**
* Twilio SMS Client Implementation<br>
* API doc: <a href=
* "https://www.twilio.com/docs/sms/api">https://www.twilio.com/docs/sms/api</a>
*/
@Slf4j
public class TwilioSmsClientImpl implements SmsClient {
private static final String API_URL_FORMAT = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json";
private static final String MESSAGE_TEMPLATE = "Instance: %s, Priority: %s, Content: %s";
private final String accountSid;
private final String authToken;
private final String twilioPhoneNumber;
public TwilioSmsClientImpl(TwilioSmsProperties config) {
if (config != null) {
this.accountSid = config.getAccountSid();
this.authToken = config.getAuthToken();
this.twilioPhoneNumber = config.getTwilioPhoneNumber();
} else {
this.accountSid = "";
this.authToken = "";
this.twilioPhoneNumber = "";
}
}
@Override
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance") == null ? alert.getGroupKey()
: alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority") == null ? "unknown"
: alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
}
this.send(receiver.getPhone(), createMessage(instance, priority, content));
}
private String createMessage(String instance, String priority, String content) {
return String.format(MESSAGE_TEMPLATE, instance, priority, content);
}
private void send(String phoneNumber, String message) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
String endpoint = String.format(API_URL_FORMAT, accountSid);
URI requestUri = new URI(endpoint);
HttpPost httpPost = createHttpPost(requestUri, phoneNumber, message);
log.info("Sending Twilio SMS request to {}", requestUri);
executeRequest(httpClient, httpPost, phoneNumber);
} catch (Exception e) {
log.warn("Failed to send SMS: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
private HttpPost createHttpPost(URI requestUri, String toNumber, String message) {
HttpPost httpPost = new HttpPost(requestUri);
String auth = accountSid + ":" + authToken;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
httpPost.setHeader("Authorization", "Basic " + encodedAuth);
List<BasicNameValuePair> parameters = new ArrayList<>();
parameters.add(new BasicNameValuePair("To", toNumber));
parameters.add(new BasicNameValuePair("From", twilioPhoneNumber));
parameters.add(new BasicNameValuePair("Body", message));
try {
httpPost.setEntity(new UrlEncodedFormEntity(parameters));
return httpPost;
} catch (Exception e) {
log.error("Failed to create HTTP request: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
private void executeRequest(CloseableHttpClient httpClient, HttpPost httpPost, String phoneNumber)
throws Exception {
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
log.info("SMS response status: {}, body: {}", statusCode, responseBody);
if (statusCode < 200 || statusCode >= 300) {
if (responseBody.contains("21608")) {
throw new SendMessageException(
"The Twilio trial account can only send SMS to verified phone numbers");
} else {
throw new SendMessageException(
"HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
}
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
if (jsonResponse == null) {
throw new SendMessageException(statusCode + ":" + responseBody);
}
JsonNode sidNode = jsonResponse.get("sid");
if (sidNode == null) {
throw new SendMessageException(statusCode + ":" + responseBody);
}
String sid = sidNode.asText();
log.info("Successfully sent SMS to phone: {}, sid: {}", phoneNumber, sid);
}
}
@Override
public String getType() {
return TWILIO;
}
@Override
public boolean checkConfig() {
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
}
}
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Test case for {@link TwilioSmsClientImpl}
*/
class TwilioSmsClientImplTest {
@Test
void getType() {
TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties();
twilioSmsProperties.setAccountSid("accountSid");
twilioSmsProperties.setAuthToken("authToken");
twilioSmsProperties.setTwilioPhoneNumber("twilioPhoneNumber");
TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties);
assertEquals("twilio", twilioSmsClient.getType());
}
@ParameterizedTest
@CsvSource({
"accountSid, authToken, twilioPhoneNumber, true",
"accountSid, authToken, '', false",
"accountSid, '', twilioPhoneNumber, false",
"accountSid, '', '', false",
"'', authToken, twilioPhoneNumber, false",
"'', authToken, '', false",
"'', '', twilioPhoneNumber, false",
"'', '', '', false",
})
void checkConfig(String accountSid, String authToken, String twilioPhoneNumber, boolean expected) {
TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties();
twilioSmsProperties.setAccountSid(accountSid);
twilioSmsProperties.setAuthToken(authToken);
twilioSmsProperties.setTwilioPhoneNumber(twilioPhoneNumber);
TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties);
assertEquals(expected, twilioSmsClient.checkConfig());
}
}
@@ -86,6 +86,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriUtils;
import org.xml.sax.InputSource;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -509,13 +510,37 @@ public class HttpCollectImpl extends AbstractCollect {
}
// params
Map<String, String> params = httpProtocol.getParams();
boolean enableUrlEncoding = Boolean.parseBoolean(httpProtocol.getEnableUrlEncoding());
StringBuilder queryParams = new StringBuilder();
if (params != null && !params.isEmpty()) {
for (Map.Entry<String, String> param : params.entrySet()) {
if (StringUtils.hasText(param.getValue())) {
requestBuilder.addParameter(param.getKey(), TimeExpressionUtil.calculate(param.getValue()));
String key = param.getKey();
String value = param.getValue();
if (!StringUtils.hasText(key)) {
continue;
}
if (queryParams.length() > 0) {
queryParams.append("&");
}
if (enableUrlEncoding) {
key = UriUtils.encodeQueryParam(key, "UTF-8");
}
queryParams.append(key);
if (StringUtils.hasText(value)) {
String calculatedValue = TimeExpressionUtil.calculate(value);
if (enableUrlEncoding) {
calculatedValue = UriUtils.encodeQueryParam(calculatedValue, "UTF-8");
}
queryParams.append("=").append(calculatedValue);
}
}
}
// The default request header can be overridden if customized
// keep-alive
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
@@ -525,8 +550,7 @@ public class HttpCollectImpl extends AbstractCollect {
if (headers != null && !headers.isEmpty()) {
for (Map.Entry<String, String> header : headers.entrySet()) {
if (StringUtils.hasText(header.getValue())) {
requestBuilder.addHeader(CollectUtil.replaceUriSpecialChar(header.getKey()),
CollectUtil.replaceUriSpecialChar(header.getValue()));
requestBuilder.addHeader(header.getKey(), header.getValue());
}
}
}
@@ -560,11 +584,29 @@ public class HttpCollectImpl extends AbstractCollect {
requestBuilder.setEntity(new StringEntity(httpProtocol.getPayload(), StandardCharsets.UTF_8));
}
// uri
String uri = CollectUtil.replaceUriSpecialChar(httpProtocol.getUrl());
if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) {
// uri encode
String uri;
if (enableUrlEncoding) {
// if the url contains parameters directly
if (httpProtocol.getUrl().contains("?")) {
String path = httpProtocol.getUrl().substring(0, httpProtocol.getUrl().indexOf("?"));
String query = httpProtocol.getUrl().substring(httpProtocol.getUrl().indexOf("?") + 1);
uri = UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8");
} else {
uri = UriUtils.encodePath(httpProtocol.getUrl(), "UTF-8");
}
} else {
uri = httpProtocol.getUrl();
}
requestBuilder.setUri(httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri);
// append query params
if (queryParams.length() > 0) {
uri += (uri.contains("?") ? "&" : "?") + queryParams.toString();
}
String finalUri;
if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) {
finalUri = httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri;
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(httpProtocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
@@ -572,11 +614,18 @@ public class HttpCollectImpl extends AbstractCollect {
: String.format("%s:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri);
boolean ssl = Boolean.parseBoolean(httpProtocol.getSsl());
if (ssl) {
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
finalUri = NetworkConstants.HTTPS_HEADER + baseUri;
} else {
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
finalUri = NetworkConstants.HTTP_HEADER + baseUri;
}
}
try {
requestBuilder.setUri(finalUri);
} catch (IllegalArgumentException e) {
log.warn("Invalid URI with illegal characters: {}. User has disabled URL encoding, not applying any encoding.", finalUri);
throw e;
}
// custom timeout
int timeout = CollectUtil.getTimeout(httpProtocol.getTimeout(), 0);
@@ -39,7 +39,7 @@ spring:
collector:
info:
version: ${COLLECTOR_VERSION:1.6.1}
version: ${COLLECTOR_VERSION:1.7.0}
ip: ${COLLECTOR_IP:}
dispatch:
entrance:
@@ -35,4 +35,7 @@ public interface SmsConstants {
// Aws cloud SMS
String AWS = "aws";
// Twilio SMS
String TWILIO = "twilio";
}
@@ -122,6 +122,11 @@ public class NoticeReceiver {
example = "779294123", accessMode = READ_WRITE)
private String tgUserId;
@Schema(title = "Telegram message thread id: The notification method is valid for Telegram Bot",
description = "TTelegram message thread id: The notification method is valid for Telegram Bot",
example = "779294123", accessMode = READ_WRITE)
private String tgMessageThreadId;
@Schema(title = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot",
description = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot",
example = "779294123", accessMode = READ_WRITE)
@@ -95,6 +95,12 @@ public class HttpProtocol implements CommonRequestProtocol, Protocol {
*/
private List<String> successCodes;
/**
* Whether to enable URL encoding for the path. Default is true.
* When true, the URL path will be encoded. When false, the URL path will not be encoded.
*/
private String enableUrlEncoding = "true";
/**
* authentication information
*/
@@ -1,40 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* Alibaba Sms Sender configuration dto
*/
@Data
public class SmsAlibabaConfig {
@NotBlank(message = "AccessKeyId cannot be empty")
private String accessKeyId;
@NotBlank(message = "AccessKeySecret cannot be empty")
private String accessKeySecret;
@NotBlank(message = "SignName cannot be empty")
private String signName;
@NotBlank(message = "TemplateCode cannot be null")
private String templateCode;
}
@@ -1,38 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* AWS Sms Sender configuration dto
*/
@Data
public class SmsAwsConfig {
@NotBlank(message = "AccessKeyId cannot be empty")
private String accessKeyId;
@NotBlank(message = "AccessKeySecret cannot be empty")
private String accessKeySecret;
@NotBlank(message = "region cannot be empty")
private String region;
}
@@ -1,48 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Tencent Sms Sender configuration dto
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SmsTencentConfig {
@NotBlank(message = "SecretId cannot be empty")
private String secretId;
@NotBlank(message = "SecretKey cannot be empty")
private String secretKey;
@NotBlank(message = "SignName cannot be empty")
private String signName;
@NotBlank(message = "AppId cannot be null")
private String appId;
@NotBlank(message = "templateId cannot be null")
private String templateId;
}
@@ -1,62 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.hertzbeat.manager.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.logging.log4j.core.config.plugins.validation.constraints.NotBlank;
/**
* UniSMS configuration
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SmsUniSmsConfig {
/**
* UniSMS access key id
*/
@NotBlank(message = "accessKeyId cannot be empty")
private String accessKeyId;
/**
* UniSMS access key secret, required for HMAC mode
*/
private String accessKeySecret;
/**
* SMS signature
*/
@NotBlank(message = "signature cannot be null")
private String signature;
/**
* SMS template ID
*/
@NotBlank(message = "templateId cannot be null")
private String templateId;
/**
* Authentication mode: simple or hmac, default is simple
*/
@NotBlank(message = "authMode cannot be null")
private String authMode = "simple";
}
@@ -75,7 +75,8 @@ import org.yaml.snakeyaml.Yaml;
/**
* Monitoring Type Management Implementation
* temporarily stores the monitoring configuration and parameter configuration in memory and then stores it in the
* Temporarily stores the monitoring configuration and parameter configuration in memory,
* and then persists it to a storage system.
*/
@Service
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@@ -22,10 +22,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Type;
import jakarta.annotation.Resource;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.support.event.SmsConfigChangeEvent;
import org.apache.hertzbeat.manager.pojo.dto.SmsNoticeSender;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@@ -35,7 +35,7 @@ import org.springframework.stereotype.Service;
*/
@Service
public class SmsGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl<SmsNoticeSender> {
public class SmsGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl<SmsConfig> {
@Resource
private ApplicationContext applicationContext;
@@ -55,7 +55,7 @@ public class SmsGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImp
* This method is used to handle the sms configuration change event.
*/
@Override
public void handler(SmsNoticeSender smsNoticeSender) {
public void handler(SmsConfig smsConfig) {
applicationContext.publishEvent(new SmsConfigChangeEvent(applicationContext));
}
@@ -69,11 +69,11 @@ public class SmsGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImp
* a TypeReference of NoticeSender type
*/
@Override
public TypeReference<SmsNoticeSender> getTypeReference() {
public TypeReference<SmsConfig> getTypeReference() {
return new TypeReference<>() {
@Override
public Type getType() {
return SmsNoticeSender.class;
return SmsConfig.class;
}
};
}
@@ -72,14 +72,22 @@ warehouse:
rpc-port: 6667
username: root
password: root
redis:
enabled: false
host: 127.0.0.1
port: 6379
password: 123456
# store real-time metrics data, enable only one below
real-time:
memory:
enabled: true
init-size: 16
redis:
enabled: false
# redis mode: single, sentinel, cluster. Default is single
mode: single
# separate each address with comma when using cluster mode, eg: 127.0.0.1:6379,127.0.0.1:6380
address: 127.0.0.1:6379
# enter master name when using sentinel mode
masterName: mymaster
password: 123456
# redis db index, default: DB0
db: 0
scheduler:
server:
@@ -102,4 +110,4 @@ ai:
# api key
api-key:
#At present, only IFLYTEK large model needs to be filled in
api-secret:
api-secret:
@@ -231,6 +231,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -17,71 +17,44 @@
-- ensure every sql can rerun without error
-- add repair table sql
REPAIR TABLE HZB_ALERT_DEFINE;
DELIMITER //
CREATE PROCEDURE AddOrDropColumns()
BEGIN
DECLARE col_exists INT;
-- Check and add 'app' column if it does not exist
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='app';
IF col_exists = 0 THEN
ALTER TABLE HZB_ALERT_DEFINE ADD COLUMN app VARCHAR(255);
END IF;
-- Drop 'app' column if it exists
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='app';
IF col_exists = 1 THEN
ALTER TABLE HZB_ALERT_DEFINE DROP COLUMN app;
END IF;
-- Check and add 'metric' column if it does not exist
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='metric';
IF col_exists = 0 THEN
ALTER TABLE HZB_ALERT_DEFINE ADD COLUMN metric VARCHAR(255);
END IF;
-- Drop 'metric' column if it exists
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='metric';
IF col_exists = 1 THEN
ALTER TABLE HZB_ALERT_DEFINE DROP COLUMN metric;
END IF;
-- Check and add 'field' column if it does not exist
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='field';
IF col_exists = 0 THEN
ALTER TABLE HZB_ALERT_DEFINE ADD COLUMN field VARCHAR(255);
END IF;
-- Drop 'field' column if it exists
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='field';
IF col_exists = 1 THEN
ALTER TABLE HZB_ALERT_DEFINE DROP COLUMN field;
END IF;
-- Check and add 'preset' column if it does not exist
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='preset';
IF col_exists = 0 THEN
ALTER TABLE HZB_ALERT_DEFINE ADD COLUMN preset BOOLEAN;
END IF;
-- Drop 'preset' column if it exists
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='preset';
IF col_exists = 1 THEN
ALTER TABLE HZB_ALERT_DEFINE DROP COLUMN preset;
END IF;
-- Check and add 'priority' column if it does not exist
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='priority';
IF col_exists = 0 THEN
ALTER TABLE HZB_ALERT_DEFINE ADD COLUMN priority INTEGER;
END IF;
-- Drop 'priority' column if it exists
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='priority';
IF col_exists = 1 THEN
ALTER TABLE HZB_ALERT_DEFINE DROP COLUMN priority;
END IF;
-- Check and add 'tags' column if it does not exist
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='tags';
IF col_exists = 0 THEN
ALTER TABLE HZB_ALERT_DEFINE ADD COLUMN tags VARCHAR(255);
END IF;
-- Drop 'tags' column if it exists
SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='HZB_ALERT_DEFINE' AND COLUMN_NAME='tags';
IF col_exists = 1 THEN
@@ -93,4 +66,5 @@ DELIMITER ;
CALL AddOrDropColumns();
DROP PROCEDURE IF EXISTS AddOrDropColumns;
DROP PROCEDURE IF EXISTS AddOrDropColumns;
commit;
@@ -246,6 +246,20 @@ params:
defaultValue: '200, 201'
# hide param-true or false
hide: true
# field-param field key
- field: enableUrlEncoding
# name-param field display i18n name
name:
zh-CN: 启用URL编码(UTF-8
en-US: Enable URL Encoding(UTF-8)
# type-param field type(most mapping the html input type)
type: boolean
# required-true or false
required: true
# default value
defaultValue: true
# hide param-true or false
hide: true
# collect metrics config list
metrics:
# metrics - summary
@@ -286,6 +300,8 @@ metrics:
method: ^_^httpMethod^_^
# if enabled https
ssl: ^_^ssl^_^
# enable url encoding
enableUrlEncoding: ^_^enableUrlEncoding^_^
# http request payload
payload: ^_^payload^_^
# http request header content
@@ -344,6 +360,8 @@ metrics:
method: GET
# if enabled https
ssl: ^_^ssl^_^
# enable url encoding
enableUrlEncoding: ^_^enableUrlEncoding^_^
authorization:
# http auth type: Basic Auth, Digest Auth, Bearer Token
type: ^_^authType^_^
@@ -21,9 +21,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.manager.pojo.dto.SmsNoticeSender;
import org.apache.hertzbeat.manager.service.impl.SmsGeneralConfigServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -66,9 +66,9 @@ class SmsGeneralConfigServiceTest {
@Test
void testGetTypeReference() {
TypeReference<SmsNoticeSender> typeReference = service.getTypeReference();
TypeReference<SmsConfig> typeReference = service.getTypeReference();
assertNotNull(typeReference);
assertEquals(SmsNoticeSender.class, typeReference.getType());
assertEquals(SmsConfig.class, typeReference.getType());
}
}
+46
View File
@@ -225,6 +225,52 @@ alerter:
Now you can configure this information in your hertzbeat application.
### Twilio SMS Configuration
To activate and use the Twilio SMS service, refer to the official Twilio documentation: [SMS Getting Started Guide](https://www.twilio.com/docs/sms/quickstart)
You can configure the Twilio SMS service either through the graphical interface or in the `application.yml` file.
To use `application.yml`, add/fill in the following Twilio SMS configuration (replace parameters with your own SMS server configuration):
```yaml
alerter:
sms:
enable: true # Whether to enable
type: twilio # SMS provider type, supports "twilio"
twilio: # Twilio SMS configuration
account-sid: # Your Twilio Account SID
auth-token: # Your Twilio Auth Token
twilio-phone-number: # Your Twilio Phone Number
```
1. Create a Twilio account
- If you don't have a Twilio account, sign up at [Twilio Console](https://www.twilio.com/console)
- You'll get a free trial account that allows you to test the SMS service
2. Obtain Twilio credentials
- After signing in, locate your Account SID and Auth Token on the dashboard
- These credentials will be used to authenticate API requests
3. Get a Twilio Phone Number
- In the Twilio Console, navigate to "Phone Numbers" > "Manage" > "Active Numbers"
- Click "Buy a Number" or use the trial number provided by Twilio
- Make sure the number has SMS capabilities enabled
- This number will be used as the sender for your SMS alerts
- A phone number is required for the Twilio SMS service to work
4. Testing Your Configuration on Twilio
- Twilio provides a sandbox environment for testing
- If you are in the trial period, you will only be able to send SMS to verified phone numbers
- To verify a phone number, add it to your verified phone numbers list in the Twilio Console
> The message follows the format: "Instance: {}, Priority: {}, Content: {}"
This information can be configured in the Hertzbeat application.
## Operation steps
1. **【Alarm notification】->【Add new recipient】 ->【Select SMS notification method】**
+1 -1
View File
@@ -9,7 +9,7 @@ keywords: [bulletin, custom]
### Add Bulletin Item
1. Click `Add New Bulletin Item`, enter the `Bulletin Name`, scroll down to select the `Monitoring Type`, select the associated `Monitoring Task Name`, and then select the `Monitoring Metrics` you want to show in the shuttle box.
1. Click `Add New Bulletin Item`, enter the `Bulletin Name`, scroll down to select the `Monitoring Type`, select the associated `Monitoring Task Name`, You can filter `Monitor Task Name` by `Label`, and then select the `Monitoring Metrics` you want to show in the shuttle box.
2. Click `OK` button to finish creating the customized bulletin.
@@ -9,7 +9,7 @@ keywords: [看板, 自定义]
### 新增看板项
1. 点击新增看板项, 输入`看板名称`, 下拉选择`监控类型`, 再选择关联的`监控任务名称`, 最后在穿梭框中选择需要展示的`监控指标`
1. 点击新增看板项, 输入`看板名称`, 下拉选择`监控类型`, 可根据`标签``监控任务名称`进行筛选,再选择关联的`监控任务名称`, 最后在穿梭框中选择需要展示的`监控指标`
2. 点击`确定`按钮, 即可完成自定义看板的创建。
+7
View File
@@ -157,6 +157,13 @@
"help/windows_script"
]
},
{
"type": "category",
"label": "server",
"items": [
"help/ipmi"
]
},
{
"type": "category",
"label": "mid",
+6
View File
@@ -448,6 +448,12 @@ export default function () {
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
</tr>
</tbody>
</table>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 79 KiB

+1 -1
View File
@@ -93,7 +93,7 @@
</modules>
<properties>
<hzb.version>1.6.1</hzb.version>
<hzb.version>1.7.0</hzb.version>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
+4
View File
@@ -230,6 +230,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -197,6 +201,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -21,20 +21,20 @@ networks:
services:
mysql:
image: mysql:8
image: mariadb:11.7
container_name: compose-mysql
hostname: mysql
restart: always
healthcheck:
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --silent']
interval: 3s
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
retries: 5
start_period: 3m
start_period: 30s
ports:
- "3306"
- '13306:3306'
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: 123456
MARIADB_ROOT_PASSWORD: 123456
volumes:
- ./dbdata/mysqldata:/var/lib/mysql/
- ./conf/sql:/docker-entrypoint-initdb.d/
@@ -55,15 +55,15 @@ services:
environment:
TZ: Asia/Shanghai
ports:
- "8181"
- "6667"
- "18181:8181"
- "16667:6667"
volumes:
- ./dbdata/iotdbdata:/iotdb/data
networks:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -193,6 +197,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -21,20 +21,20 @@ networks:
services:
mysql:
image: mysql:8
image: mariadb:11.7
container_name: compose-mysql
hostname: mysql
restart: always
healthcheck:
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --silent']
interval: 3s
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
retries: 5
start_period: 3m
start_period: 30s
ports:
- "3306"
- '13306:3306'
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: 123456
MARIADB_ROOT_PASSWORD: 123456
volumes:
- ./dbdata/mysqldata:/var/lib/mysql/
- ./conf/sql:/docker-entrypoint-initdb.d/
@@ -55,14 +55,14 @@ services:
environment:
TZ: Asia/Shanghai
ports:
- "6041"
- "16041:6041"
volumes:
- ./dbdata/taosdata:/var/lib/taos/
networks:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -191,6 +195,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -21,20 +21,20 @@ networks:
services:
mysql:
image: mysql:8
image: mariadb:11.7
container_name: compose-mysql
hostname: mysql
restart: always
healthcheck:
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --silent']
interval: 3s
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
retries: 5
start_period: 3m
start_period: 30s
ports:
- "3306"
- '13306:3306'
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: 123456
MARIADB_ROOT_PASSWORD: 123456
volumes:
- ./dbdata/mysqldata:/var/lib/mysql/
- ./conf/sql:/docker-entrypoint-initdb.d/
@@ -55,14 +55,14 @@ services:
environment:
TZ: Asia/Shanghai
ports:
- "8428"
- "18428:8428"
volumes:
- ./dbdata/tsdbdata:/victoria-metrics-data
networks:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -190,6 +194,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -32,14 +32,15 @@ services:
retries: 5
start_period: 30s
ports:
- '5432:5432'
- '15432:5432'
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: 123456
TZ: Asia/Shanghai
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- ./dbdata/pgdata/data:/var/lib/postgresql/data
- ./conf/sql:/docker-entrypoint-initdb.d/
- ./dbdata/pgdata:/var/lib/postgresql/data
- ./conf/sql:/docker-entrypoint-initdb.d/
networks:
- hertzbeat
@@ -57,14 +58,14 @@ services:
environment:
TZ: Asia/Shanghai
ports:
- "8428"
- "18428:8428"
volumes:
- ./dbdata/tsdbdata:/victoria-metrics-data
networks:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
+2 -2
View File
@@ -19,11 +19,11 @@ $ docker buildx use mybuilder
```shell
# Use the --platform flag to specify the target platforms for the build --push flag to push the image to a registry
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:v1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:latest . --push
# Inspect the image info
$ docker buildx imagetools inspect apache/hertzbeat-collector:v1.0.0
$ docker buildx imagetools inspect apache/hertzbeat-collector:1.0.0
```
+3 -3
View File
@@ -31,17 +31,17 @@ fi
# docker compile context
CONTEXT_DIR=`pwd`
COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
# Build Local
#COMMAND="docker build -t apache/hertzbeat-collector:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR "
#COMMAND="docker build -t apache/hertzbeat-collector:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" "
echo "$COMMAND"
+2 -2
View File
@@ -19,10 +19,10 @@ $ docker buildx use mybuilder
```shell
# Use the --platform flag to specify the target platforms for the build --push flag to push the image to a registry
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat:v1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat:1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:latest . --push
# Inspect the image info
$ docker buildx imagetools inspect apache/hertzbeat:v1.0.0
$ docker buildx imagetools inspect apache/hertzbeat:1.0.0
```
+4 -8
View File
@@ -27,23 +27,19 @@ VERSION=`ls apache-hertzbeat-*-incubating-docker-compose.tar.gz| awk -F"-" '{pri
# compile context dir
CONTEXT_DIR=`pwd`
COMMAND="docker buildx build --platform ${IMAGE_PLATFORM:-linux/arm64,linux/amd64} -t apache/hertzbeat:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push=${IMAGE_PUSH:-true} --load=${IMAGE_LOAD:-false}"
COMMAND="docker buildx build --platform ${IMAGE_PLATFORM:-linux/arm64,linux/amd64} -t apache/hertzbeat:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push=${IMAGE_PUSH:-true} --load=${IMAGE_LOAD:-false}"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
# Build Local
#COMMAND="docker build -t apache/hertzbeat:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR "
#COMMAND="docker build -t apache/hertzbeat:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR "
echo "$COMMAND"
$COMMAND
COMMAND="docker tag apache/hertzbeat:v$VERSION apache/hertzbeat:${IMAGE_VERSION:-latest}"
#COMMAND="docker tag apache/hertzbeat:$VERSION apache/hertzbeat:${IMAGE_VERSION:-latest}"
echo "$COMMAND"
+1 -1
View File
@@ -17,7 +17,7 @@ $gpgKeyId = "********"
# ********************************************************
# Set version and release candidate number
$version = "1.6.1"
$version = "1.7.0"
$rcNumber = "rc1"
# ********************************************************
+2 -2
View File
@@ -86,8 +86,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
"maximumWarning": "50kb",
"maximumError": "100kb"
}
]
},
+1
View File
@@ -27,6 +27,7 @@ export class NoticeReceiver {
email!: string;
tgBotToken!: string;
tgUserId!: string;
tgMessageThreadId!: string;
userId!: string;
slackWebHookUrl!: string;
discordChannelId!: string;
+2
View File
@@ -21,6 +21,7 @@ import { AlibabaSmsConfig } from './AlibabaSmsConfig';
import { AwsSmsConfig } from './AwsSmsConfig';
import { SmslocalSmsConfig } from './SmslocalSmsConfig';
import { TencentSmsConfig } from './TencentSmsConfig';
import { TwilioSmsConfig } from './TwilioSmsConfig';
import { UniSmsConfig } from './UniSmsConfig';
import { SmsType } from './enums/sms-type.enum';
@@ -32,6 +33,7 @@ export class SmsNoticeSender {
unisms: UniSmsConfig = new UniSmsConfig();
smslocal: SmslocalSmsConfig = new SmslocalSmsConfig();
aws: AwsSmsConfig = new AwsSmsConfig();
twilio: TwilioSmsConfig = new TwilioSmsConfig();
enable: boolean = false;
creator!: string;
modifier!: string;
@@ -17,22 +17,8 @@
* under the License.
*/
package org.apache.hertzbeat.manager.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Smslocal SMS configuration
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SmslocalConfig {
/**
* Smslocal api key
*/
private String apiKey;
export class TwilioSmsConfig {
accountSid!: string;
authToken!: string;
twilioPhoneNumber!: string;
}
+2 -1
View File
@@ -22,7 +22,8 @@ export enum SmsType {
ALIBABA = 'alibaba',
UNISMS = 'unisms',
SMSLOCAL = 'smslocal',
AWS = 'aws'
AWS = 'aws',
TWILIO = 'twilio'
}
export enum UniSmsAuthMode {
@@ -163,7 +163,15 @@
{{ 'alert.inhibit.source_labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<app-labels-input [(ngModel)]="alertInhibit.sourceLabels" name="sourceLabels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'sourceLabels'"
[(ngModel)]="alertInhibit.sourceLabels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -171,7 +179,15 @@
{{ 'alert.inhibit.target_labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<app-labels-input [(ngModel)]="alertInhibit.targetLabels" name="targetLabels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'targetLabels'"
[(ngModel)]="alertInhibit.targetLabels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -67,12 +67,12 @@ export class AlertIntegrationComponent implements OnInit {
id: 'skywalking',
name: this.i18nSvc.fanyi('alert.integration.source.skywalking'),
icon: 'assets/img/integration/skywalking.svg'
},
{
id: 'tencent',
name: this.i18nSvc.fanyi('alert.integration.source.tencent'),
icon: 'assets/img/integration/tencent.svg'
}
// {
// id: 'tencent',
// name: this.i18nSvc.fanyi('alert.integration.source.tencent'),
// icon: 'assets/img/integration/tencent.svg'
// }
];
selectedSource: DataSource | null = null;
@@ -344,6 +344,12 @@
<input [(ngModel)]="receiver.tgUserId" [required]="receiver.type === 7" name="tgUserId" nz-input type="text" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 7">
<nz-form-label [nzSpan]="7" nzFor="tgMessageThreadId">{{ 'alert.notice.type.telegram-message-thread-id' | i18n }}</nz-form-label>
<nz-form-control [nzErrorTip]="'validation.required' | i18n" [nzSpan]="12">
<input [(ngModel)]="receiver.tgMessageThreadId" name="tgMessageThreadId" nz-input type="text" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 8">
<nz-form-label [nzRequired]="receiver.type === 8" [nzSpan]="7" nzFor="slackWebHookUrl"
>{{ 'alert.notice.type.slack-webHook-url' | i18n }}
@@ -186,7 +186,15 @@
<nz-form-item *ngIf="!rule.filterAll">
<nz-form-label nzFor="labels" nzSpan="7" nzRequired="true">{{ 'alert.notice.rule.tag' | i18n }} </nz-form-label>
<nz-form-control nzSpan="12" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input [(ngModel)]="rule.labels" name="labels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'labels'"
[(ngModel)]="rule.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -538,7 +538,14 @@
<nz-form-item>
<nz-form-label nzSpan="7" [nzFor]="'labels'" [nzTooltipTitle]="'label.bind.tip' | i18n">{{ 'label.bind' | i18n }} </nz-form-label>
<nz-form-control nzSpan="12">
<app-labels-input [(ngModel)]="define.labels" name="labels"></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
}"
[name]="'labels'"
[(ngModel)]="define.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -546,7 +553,14 @@
>{{ 'common.annotation.bind' | i18n }}
</nz-form-label>
<nz-form-control nzSpan="12">
<app-labels-input [(ngModel)]="define.annotations" name="annotations"></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
}"
[name]="'annotations'"
[(ngModel)]="define.annotations"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -848,7 +848,7 @@ export class AlertSettingComponent implements OnInit {
if (!this.define.labels) {
this.define.labels = {};
}
this.define.labels['severity'] = this.severity;
this.define.labels = { ...this.define.labels, severity: this.severity };
}
onManageModalCancel() {
@@ -179,7 +179,15 @@
<nz-form-item *ngIf="!silence.matchAll">
<nz-form-label nzSpan="7" nzFor="labels" nzRequired="true">{{ 'alert.silence.labels' | i18n }}</nz-form-label>
<nz-form-control nzSpan="12" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input [(ngModel)]="silence.labels" name="labels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'labels'"
[(ngModel)]="silence.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -181,6 +181,21 @@
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzSpan="7" [nzFor]="'filterLabels'">{{ 'monitor.search.label' | i18n }} </nz-form-label>
<nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' | i18n">
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
}"
[name]="'labels'"
[(ngModel)]="filterLabels"
(ngModelChange)="onFilterInputChange($event)"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="dropdown" nzRequired="true">{{ 'bulletin.monitor.name' | i18n }}</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
@@ -193,7 +208,7 @@
nzMode="multiple"
required
>
<ng-container *ngFor="let monitor of monitors">
<ng-container *ngFor="let monitor of filteredMonitors">
<nz-option *ngIf="isMonitorListLoading" [nzValue]="monitor.id" [nzLabel]="monitor.name"></nz-option>
</ng-container>
</nz-select>
@@ -25,7 +25,8 @@ import { NzNotificationService } from 'ng-zorro-antd/notification';
import { NzTableQueryParams } from 'ng-zorro-antd/table';
import { TransferChange } from 'ng-zorro-antd/transfer';
import { NzFormatEmitEvent, NzTreeNode, NzTreeNodeOptions } from 'ng-zorro-antd/tree';
import { finalize } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { finalize, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { BulletinDefine } from '../../pojo/BulletinDefine';
import { Fields } from '../../pojo/Fields';
@@ -73,22 +74,35 @@ export class BulletinComponent implements OnInit, OnDestroy {
refreshInterval: any;
deadline = 30;
countDownTime: number = 0;
filterLabels: Record<string, string> = {};
filteredMonitors: Monitor[] = [];
private filterSubject = new Subject<string>(); //filter logic debouncing
ngOnInit() {
this.loadTabs();
this.refreshInterval = setInterval(() => {
this.countDown();
}, 1000); // every 30 seconds refresh the tabs
this.filterSubject
.pipe(
debounceTime(300), // triggered after the user stops typing for 300ms
distinctUntilChanged()
)
.subscribe(value => {
this.filterMonitors();
});
}
ngOnDestroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
this.filterSubject.complete();
}
sync() {
this.loadCurrentBulletinData();
this.clearFilters();
}
configRefreshDeadline(deadlineTime: number) {
@@ -158,6 +172,7 @@ export class BulletinComponent implements OnInit, OnDestroy {
this.isManageModalVisible = false;
// clear fields
this.fields = {};
this.clearFilters();
}
resetManageModalData() {
@@ -254,6 +269,7 @@ export class BulletinComponent implements OnInit, OnDestroy {
message => {
if (message.code === 0) {
this.monitors = message.data;
this.filterMonitors();
if (this.monitors != null) {
this.isMonitorListLoading = true;
}
@@ -568,4 +584,47 @@ export class BulletinComponent implements OnInit, OnDestroy {
}
return result;
}
onFilterInputChange(value: string): void {
this.filterSubject.next(value); // push the input value to the debounce Subject
}
filterMonitors(): void {
const validLabels = this.cleanFilterLabels(this.filterLabels);
const activeFilters = Object.entries(validLabels).filter(([k, v]) => k !== '' && k !== 'null'); // Second filtering ensures key validity
if (activeFilters.length === 0) {
this.filteredMonitors = [...this.monitors];
return;
}
this.filteredMonitors = this.monitors.filter(monitor => {
if (!monitor.labels || typeof monitor.labels !== 'object') return false;
return activeFilters.every(([filterKey, filterValue]) => {
const keyExists = Object.prototype.hasOwnProperty.call(monitor.labels, filterKey);
const actualValue = (monitor.labels[filterKey] ?? '').toLowerCase();
return filterValue === '' ? keyExists : actualValue.includes(filterValue.toLowerCase());
});
});
}
private cleanFilterLabels(labels: any): Record<string, string> {
const cleaned: Record<string, string> = {};
Object.entries(labels ?? {}).forEach(([k, v]) => {
if (typeof k === 'string') {
const trimmedKey = k.trim();
if (trimmedKey !== '' && trimmedKey !== 'null') {
cleaned[trimmedKey] = (v ?? '').toString().trim();
}
}
});
return cleaned;
}
clearFilters(): void {
this.filterLabels = {};
this.filterMonitors();
this.cdr.detectChanges();
}
}
@@ -131,12 +131,14 @@
<nz-form-item>
<nz-form-label nzSpan="7" [nzFor]="'labels'" [nzTooltipTitle]="'label.bind.tip' | i18n">{{ 'label.bind' | i18n }} </nz-form-label>
<nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
}"
[name]="'labels'"
[(ngModel)]="monitor.labels"
name="labels"
[keyAlias]="'placeholder.key' | i18n"
[valueAlias]="'placeholder.value' | i18n"
></app-labels-input>
/>
</nz-form-control>
</nz-form-item>
@@ -145,12 +147,14 @@
>{{ 'common.annotation.bind' | i18n }}
</nz-form-label>
<nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input
<app-form-field
[item]="{
field: 'annotations',
type: 'labels'
}"
[name]="'annotations'"
[(ngModel)]="monitor.annotations"
name="annotation"
[keyAlias]="'placeholder.key' | i18n"
[valueAlias]="'placeholder.value' | i18n"
></app-labels-input>
/>
</nz-form-control>
</nz-form-item>
@@ -72,6 +72,9 @@ export class MonitorListComponent implements OnInit, OnDestroy {
appSearchOrigin: any[] = [];
appSearchLoading = false;
intervalId: any;
// save the current sorting status
currentSortField: string | null = null;
currentSortOrder: string | null = null;
switchExportTypeModalFooter: ModalButtonOptions[] = [
{ label: this.i18nSvc.fanyi('common.button.cancel'), type: 'default', onClick: () => (this.isSwitchExportTypeModalVisible = false) }
@@ -149,7 +152,7 @@ export class MonitorListComponent implements OnInit, OnDestroy {
}
sync() {
this.loadMonitorTable();
this.loadMonitorTable(this.currentSortField, this.currentSortOrder);
}
getAppIconName(app: string | undefined): string {
@@ -499,9 +502,9 @@ export class MonitorListComponent implements OnInit, OnDestroy {
this.pageIndex = pageIndex;
this.pageSize = pageSize;
const currentSort = sort.find(item => item.value !== null);
const sortField = (currentSort && currentSort.key) || null;
const sortOrder = (currentSort && currentSort.value) || null;
this.changeMonitorTable(sortField, sortOrder);
this.currentSortField = (currentSort && currentSort.key) || null;
this.currentSortOrder = (currentSort && currentSort.value) || null;
this.changeMonitorTable(this.currentSortField, this.currentSortOrder);
}
// begin: app type search filter
@@ -72,6 +72,11 @@
<br />
{{ 'alert.notice.sender.sms.unisms.authMode' | i18n }}: {{ smsNoticeSender.unisms.authMode }}
</ng-container>
<ng-container *ngSwitchCase="SmsType.TWILIO">
{{ 'alert.notice.sender.sms.twilio.accountSid' | i18n }}: {{ smsNoticeSender.twilio.accountSid }}
<br />
{{ 'alert.notice.sender.sms.twilio.twilioPhoneNumber' | i18n }}: {{ smsNoticeSender.twilio.twilioPhoneNumber }}
</ng-container>
</ng-container>
<br />
{{ 'common.enable' | i18n }}: {{ smsNoticeSender.enable ? ('common.yes' | i18n) : ('common.no' | i18n) }}
@@ -160,6 +165,7 @@
<nz-option [nzValue]="SmsType.UNISMS" nzLabel="{{ 'alert.notice.sender.sms.type.unisms' | i18n }}"></nz-option>
<nz-option [nzValue]="SmsType.SMSLOCAL" nzLabel="{{ 'alert.notice.sender.sms.type.smslocal' | i18n }}"></nz-option>
<nz-option [nzValue]="SmsType.AWS" nzLabel="{{ 'alert.notice.sender.sms.type.aws' | i18n }}"></nz-option>
<nz-option [nzValue]="SmsType.TWILIO" nzLabel="{{ 'alert.notice.sender.sms.type.twilio' | i18n }}"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
@@ -373,6 +379,48 @@
</nz-form-item>
</ng-container>
<!-- Twilio SMS -->
<ng-container *ngIf="smsType === SmsType.TWILIO">
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="accountSid" nzRequired="true">
{{ 'alert.notice.sender.sms.twilio.accountSid' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input
[(ngModel)]="smsNoticeSender.twilio.accountSid"
nz-input
required
name="accountSid"
type="password"
id="twilioAccountSid"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="authToken" nzRequired="true">
{{ 'alert.notice.sender.sms.twilio.authToken' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input [(ngModel)]="smsNoticeSender.twilio.authToken" nz-input required name="authToken" type="password" id="twilioAuthToken" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="twilioPhoneNumber" nzRequired="true">
{{ 'alert.notice.sender.sms.twilio.twilioPhoneNumber' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input
[(ngModel)]="smsNoticeSender.twilio.twilioPhoneNumber"
nz-input
required
name="twilioPhoneNumber"
type="text"
id="twilioPhoneNumber"
/>
</nz-form-control>
</nz-form-item>
</ng-container>
<nz-form-item>
<nz-form-label nzSpan="7" nzFor="smsEnable" nzRequired="true">{{ 'common.enable' | i18n }}</nz-form-label>
<nz-form-control nzSpan="12">
@@ -33,6 +33,7 @@ import { SmsType, UniSmsAuthMode } from 'src/app/pojo/enums/sms-type.enum';
import { AwsSmsConfig } from '../../../../pojo/AwsSmsConfig';
import { EmailNoticeSender } from '../../../../pojo/EmailNoticeSender';
import { SmslocalSmsConfig } from '../../../../pojo/SmslocalSmsConfig';
import { TwilioSmsConfig } from '../../../../pojo/TwilioSmsConfig';
import { GeneralConfigService } from '../../../../service/general-config.service';
@Component({
@@ -146,6 +147,7 @@ export class MessageServerComponent implements OnInit {
this.smsNoticeSender.unisms = { ...new UniSmsConfig(), ...message.data.unisms };
this.smsNoticeSender.smslocal = { ...new SmslocalSmsConfig(), ...message.data.smslocal };
this.smsNoticeSender.aws = { ...new AwsSmsConfig(), ...message.data.aws };
this.smsNoticeSender.twilio = { ...new TwilioSmsConfig(), ...message.data.twilio };
this.smsType = message.data.type || 'tencent';
} else {
this.smsNoticeSender = new SmsNoticeSender();
@@ -171,7 +173,8 @@ export class MessageServerComponent implements OnInit {
alibaba: { ...this.smsNoticeSender.alibaba },
unisms: { ...this.smsNoticeSender.unisms },
smslocal: { ...this.smsNoticeSender.smslocal },
aws: { ...this.smsNoticeSender.aws }
aws: { ...this.smsNoticeSender.aws },
twilio: { ...this.smsNoticeSender.twilio }
};
this.isSmsServerModalVisible = true;
}
@@ -184,7 +187,8 @@ export class MessageServerComponent implements OnInit {
alibaba: { ...this.tempSmsNoticeSender.alibaba },
unisms: { ...this.tempSmsNoticeSender.unisms },
smslocal: { ...this.tempSmsNoticeSender.smslocal },
aws: { ...this.tempSmsNoticeSender.aws }
aws: { ...this.tempSmsNoticeSender.aws },
twilio: { ...this.tempSmsNoticeSender.twilio }
};
this.isSmsServerModalVisible = false;
}
@@ -391,7 +391,15 @@
'status.component.tag' | i18n
}}</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input [(ngModel)]="currentStatusComponent.labels" name="sourceLabels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'sourceLabels'"
[(ngModel)]="currentStatusComponent.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="currentStatusComponent.method == 1">
@@ -0,0 +1,43 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<div class="container">
<div *ngFor="let obj of value; let i = index" class="row">
<ng-container *ngFor="let cfg of configs">
<app-multi-func-input
[style]="cfg.style"
[name]="(name || '') + '-' + i + '-' + cfg.key"
[id]="(id || '') + '-' + i + '-' + cfg.key"
[required]="cfg.required ?? false"
[placeholder]="cfg.placeholder ?? ''"
[(value)]="value[i][cfg.key]"
(valueChange)="onChange(i, cfg.key, $event)"
/>
<span *ngIf="cfg.separatorAfter" class="separator">{{ cfg.separatorAfter }}</span>
</ng-container>
<div class="btns">
<button nz-button nzSize="small" *ngIf="value.length > 1" (click)="del(i, $event)">
<i nz-icon nzType="minus" nzTheme="outline"></i>
</button>
<button nz-button nzSize="small" (click)="add($event)">
<i nz-icon nzType="plus" nzTheme="outline"></i>
</button>
</div>
</div>
</div>
@@ -0,0 +1,31 @@
.container {
display: flex;
flex-direction: column;
gap: 5px;
}
.row {
flex: 1;
display: flex;
gap: 5px;
app-multi-func-input {
flex: 1;
}
}
.separator {
flex-basis: min-content;
text-align: center;
align-content: center;
}
.btns {
display: flex;
align-items: center;
gap: 5px;
button {
margin-left: 0px!important;
}
}
@@ -19,20 +19,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MetricsFieldInputComponent } from './metrics-field-input.component';
import { ConfigurableFieldComponent } from './configurable-field.component';
describe('MetricsFieldInputComponent', () => {
let component: MetricsFieldInputComponent;
let fixture: ComponentFixture<MetricsFieldInputComponent>;
describe('ConfigurableFieldComponent', () => {
let component: ConfigurableFieldComponent;
let fixture: ComponentFixture<ConfigurableFieldComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MetricsFieldInputComponent]
declarations: [ConfigurableFieldComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MetricsFieldInputComponent);
fixture = TestBed.createComponent(ConfigurableFieldComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -0,0 +1,115 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, Input } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
export interface FieldConfig {
key: string;
style?: string;
required?: boolean;
placeholder?: string;
separatorAfter?: string;
validators?: any[];
}
@Component({
selector: 'app-configurable-field',
templateUrl: './configurable-field.component.html',
styleUrls: ['./configurable-field.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: ConfigurableFieldComponent
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: ConfigurableFieldComponent
}
]
})
export class ConfigurableFieldComponent implements ControlValueAccessor, Validator {
constructor() {}
@Input() configs!: FieldConfig[];
@Input() name!: string;
@Input() id!: string;
value: any = [{}];
validateStatus!: string;
_onChange: Function = () => {};
_onTouched: Function = () => {};
writeValue(value: any): void {
this.value = value && value.length > 0 ? value : [{}];
}
registerOnChange(fn: Function): void {
this._onChange = fn;
}
registerOnTouched(fn: Function): void {
this._onTouched = fn;
}
validate(control: AbstractControl): ValidationErrors | null {
let value = control?.value ?? [];
for (let o of value) {
for (let c of this.configs) {
let v = o[c.key];
if (c.required && (v === null || v === undefined || (typeof v === 'string' && v.trim() === ''))) {
this.validateStatus = 'error';
return {
required: {
valid: false
}
};
}
}
}
this.validateStatus = '';
return null;
}
onChange(index: number, key: string, value: any) {
this.value[index][key] = value;
this._onChange(this.value);
this._onTouched();
}
add(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.value.push({});
this._onChange(this.value);
}
del(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.value.length > 1) {
this.value.splice(index, 1);
}
this._onChange(this.value);
}
}
@@ -89,23 +89,70 @@
</label>
</nz-radio-group>
<app-key-value-input
*ngIf="item.type === 'key-value'"
<app-configurable-field
*ngIf="item.type === 'labels'"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
[keyAlias]="'monitor.headerName.tip' | i18n"
[valueAlias]="'monitor.headerValue.tip' | i18n"
[configs]="[
{
key: 'key',
required: true,
separatorAfter: ':',
placeholder: 'placeholder.key' | i18n
},
{
key: 'value',
required: true,
placeholder: 'placeholder.value' | i18n
}
]"
[name]="item.field"
[id]="item.field"
/>
<app-metrics-field-input
<app-configurable-field
*ngIf="item.type === 'key-value'"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
[configs]="[
{
key: 'key',
required: true,
placeholder: 'monitor.headerName.tip' | i18n
},
{
key: 'value',
required: true,
placeholder: 'monitor.headerValue.tip' | i18n
}
]"
[name]="item.field"
[id]="item.field"
/>
<app-configurable-field
*ngIf="item.type === 'metrics-field'"
[(value)]="value"
(valueChange)="onChange($event)"
[FieldAlias]="'Field'"
[UnitAlias]="'Unit'"
[TypeAlias]="'Type'"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
[configs]="[
{
key: 'field',
required: true,
placeholder: 'Field'
},
{
key: 'unit',
required: true,
placeholder: 'Unit',
style: 'flex: 0; flex-basis: min-content; min-width: 50px'
},
{
key: 'type',
required: true,
placeholder: 'Type'
}
]"
[name]="item.field"
[id]="item.field"
/>
@@ -17,9 +17,11 @@
* under the License.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, Input, ViewChild } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { ConfigurableFieldComponent } from '../configurable-field/configurable-field.component';
@Component({
selector: 'app-form-field',
templateUrl: './form-field.component.html',
@@ -41,15 +43,39 @@ export class FormFieldComponent implements ControlValueAccessor, Validator {
constructor() {}
@Input() item!: any;
@Input() extra: any = {};
@ViewChild(ConfigurableFieldComponent) configurableField: ConfigurableFieldComponent | undefined;
value: any;
validateStatus!: string;
_onChange: Function = () => {};
_onTouched: Function = () => {};
writeValue(value: any): void {
this.value = value;
if (this.item.type === 'key-value' && value && typeof value === 'string') {
// It is compatible with the key-value type data structure of existing data. For the conversion from the old to the new:
// Old structure: "{"1":"2","3":"4"}"
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
let tmpValue = JSON.parse(value);
let newValue = [];
for (let k in tmpValue) {
newValue[newValue.length] = {
key: k,
value: tmpValue[k]
};
}
this.value = newValue;
} else if (this.item.type === 'labels' && value && !(value instanceof Array)) {
// It is compatible with the lables type data structure of existing data. For the conversion from the old to the new:
// Old structure: {1:"2", 3:"4"}
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
let newValue = Object.entries(value).map(([key, value]) => ({
key: String(key),
value: String(value)
}));
this.value = newValue;
} else {
this.value = value;
}
}
registerOnChange(fn: Function): void {
@@ -63,19 +89,44 @@ export class FormFieldComponent implements ControlValueAccessor, Validator {
validate(control: AbstractControl): ValidationErrors | null {
// if (!(control.dirty) && !(control.touched)) return null;
let { value } = control;
if (this.item.required && (value === null || value === undefined || value === '')) {
this.validateStatus = 'error';
if (
this.item.required &&
(value === null ||
value === undefined ||
(typeof value === 'string' && value.trim() === '') ||
this.configurableField?.validateStatus === 'error')
) {
return {
required: {
valid: false
}
};
}
this.validateStatus = '';
return null;
}
onChange(value: any) {
this._onChange(value);
if (this.item.type === 'key-value' && value) {
// It is compatible with the key-value type data structure of existing data. For the conversion from the new to the old:
// Old structure: "{"1":"2","3":"4"}"
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
const newValue: any = {};
value.forEach((item: any) => {
newValue[item.key] = item.value;
});
this._onChange(JSON.stringify(newValue));
} else if (this.item.type === 'labels' && value) {
// It is compatible with the lables type data structure of existing data. For the conversion from the new to the old:
// Old structure: {1:"2", 3:"4"}
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
const newValue: any = {};
value.forEach((item: any) => {
newValue[item.key] = item.value;
});
this._onChange(newValue);
} else {
this._onChange(value);
}
this._onTouched();
}
}
@@ -1,31 +0,0 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<div style="margin-bottom: 2px; margin-top: 2px" *ngFor="let item of keyValues; let i = index" nz-row>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="keyAlias" [(ngModel)]="item.key" (ngModelChange)="emitChange()" />
</div>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="valueAlias" [(ngModel)]="item.value" (ngModelChange)="emitChange()" />
</div>
<div nz-col nzSpan="4">
<i nz-icon nzType="plus-circle" class="dynamic-button" *ngIf="i === 0" (click)="addNew($event)"></i>
<i nz-icon nzType="minus-circle" class="dynamic-button" (click)="removeCurrent(i, $event)"></i>
</div>
</div>
@@ -1,12 +0,0 @@
.dynamic-button {
position: relative;
top: 20%;
margin-left: 12%;
font-size: 15px;
cursor: pointer;
transition: all 0.3s;
}
.dynamic-button:hover {
transform: scale(1.2);
}
@@ -1,43 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KeyValueInputComponent } from './key-value-input.component';
describe('KeyValueInputComponent', () => {
let component: KeyValueInputComponent;
let fixture: ComponentFixture<KeyValueInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [KeyValueInputComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(KeyValueInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,119 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-key-value-input',
templateUrl: './key-value-input.component.html',
styleUrls: ['./key-value-input.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyValueInputComponent),
multi: true
}
]
})
export class KeyValueInputComponent implements OnInit, ControlValueAccessor {
constructor() {}
@Input()
keyAlias: string = 'Key';
@Input()
valueAlias: string = 'Value';
keyValues: any[] = [];
// ControlValueAccessor
private onChange: any = () => {};
private onTouched: any = () => {};
ngOnInit(): void {
if (this.keyValues.length === 0) {
this.addNew();
}
}
writeValue(obj: string): void {
if (obj != undefined) {
let value = JSON.parse(obj);
if (value != null) {
this.keyValues = [];
Object.keys(value).map(item => {
this.keyValues.push({
key: item,
value: value[item]
});
});
}
}
if (this.keyValues.length === 0) {
this.addNew();
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// set disabled state here
}
addNew(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.keyValues.push({
key: '',
value: ''
});
}
removeCurrent(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.keyValues.length > 1) {
this.keyValues.splice(index, 1);
this.emitChange();
}
}
emitChange() {
let value: Record<string, string> = {};
this.keyValues.forEach(item => {
if (item != null && item.key != null) {
value[item.key] = item.value;
}
});
if (Object.keys(value).length === 0 || Object.values(value).every(v => v === '')) {
this.onChange(null);
} else {
this.onChange(JSON.stringify(value));
}
this.onTouched();
}
}
@@ -1,34 +0,0 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<div style="margin-bottom: 2px; margin-top: 2px" *ngFor="let item of keyValues; let i = index" nz-row>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="keyAlias" [(ngModel)]="item.key" (ngModelChange)="emitChange()" />
</div>
<div style="text-align: center; font-weight: bolder" nz-col nzSpan="1">
<span>:</span>
</div>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="valueAlias" [(ngModel)]="item.value" (ngModelChange)="emitChange()" />
</div>
<div nz-col nzSpan="3">
<i nz-icon nzType="plus-circle" class="dynamic-button" *ngIf="i === 0" (click)="addNew($event)"></i>
<i nz-icon nzType="minus-circle" class="dynamic-button" (click)="removeCurrent(i, $event)"></i>
</div>
</div>
@@ -1,12 +0,0 @@
.dynamic-button {
position: relative;
top: 20%;
margin-left: 12%;
font-size: 15px;
cursor: pointer;
transition: all 0.3s;
}
.dynamic-button:hover {
transform: scale(1.2);
}
@@ -1,41 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LabelsInputComponent } from './labels-input.component';
describe('LabelsInputComponent', () => {
let component: LabelsInputComponent;
let fixture: ComponentFixture<LabelsInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LabelsInputComponent]
}).compileComponents();
fixture = TestBed.createComponent(LabelsInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,115 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, Input, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
interface KeyValuePair {
key: string;
value: string;
}
@Component({
selector: 'app-labels-input',
templateUrl: './labels-input.component.html',
styleUrl: './labels-input.component.less',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => LabelsInputComponent),
multi: true
}
]
})
export class LabelsInputComponent implements OnInit, ControlValueAccessor {
constructor() {}
@Input() keyAlias: string = 'Key';
@Input() valueAlias: string = 'Value';
keyValues: KeyValuePair[] = [];
// ControlValueAccessor
private onChange: any = () => {};
private onTouched: any = () => {};
writeValue(value: Record<string, string>): void {
this.keyValues = Object.entries(value || {}).map(([key, value]) => ({
key,
value
}));
if (this.keyValues.length === 0) {
this.addNew();
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// set disabled state here
}
ngOnInit(): void {
if (this.keyValues.length === 0) {
this.addNew();
}
}
addNew(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.keyValues.push({
key: '',
value: ''
});
}
removeCurrent(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.keyValues.length > 1) {
this.keyValues.splice(index, 1);
this.emitChange();
}
}
emitChange() {
const result: Record<string, string> = {};
this.keyValues.forEach(item => {
if (item.key?.trim()) {
result[item.key] = item.value || '';
}
});
if (Object.keys(result).length === 0 || Object.values(result).every(v => v === '')) {
this.onChange(null);
} else {
this.onChange(result);
}
this.onTouched();
}
}
@@ -1,34 +0,0 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<div *ngFor="let item of fields; let i = index" nz-row>
<div nz-col nzSpan="7">
<input nz-input [placeholder]="FieldAlias" [(ngModel)]="item.field" (ngModelChange)="onChange()" />
</div>
<div nz-col nzSpan="7">
<input nz-input [placeholder]="UnitAlias" [(ngModel)]="item.unit" (ngModelChange)="onChange()" />
</div>
<div nz-col nzSpan="7">
<input nz-input [placeholder]="TypeAlias" [(ngModel)]="item.type" (ngModelChange)="onChange()" />
</div>
<div nz-col nzSpan="3">
<i nz-icon nzType="plus-circle" class="dynamic-button" *ngIf="i === 0" (click)="addNew($event)"></i>
<i nz-icon nzType="minus-circle" class="dynamic-button" (click)="removeCurrent(i, $event)"></i>
</div>
</div>
@@ -1,12 +0,0 @@
.dynamic-button {
position: relative;
top: 20%;
margin-left: 12%;
font-size: 15px;
cursor: pointer;
transition: all 0.3s;
}
.dynamic-button:hover {
transform: scale(1.7);
}
@@ -1,87 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
@Component({
selector: 'app-metrics-field-input',
templateUrl: './metrics-field-input.component.html',
styleUrls: ['./metrics-field-input.component.less']
})
export class MetricsFieldInputComponent implements OnInit {
constructor() {}
@Input() value!: any;
@Output() readonly valueChange = new EventEmitter<string>();
@Input()
FieldAlias: string = 'field';
@Input()
UnitAlias: string = 'unit';
@Input()
TypeAlias: string = 'type';
fields: any[] = [];
ngOnInit(): void {
if (this.value == undefined) {
this.fields.push({
field: '',
unit: '',
type: ''
});
} else {
this.value = JSON.parse(this.value);
}
if (this.value) {
for (let item of this.value) {
this.fields.push({
field: item.field,
unit: item.unit,
type: item.type
});
}
}
}
addNew(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.fields.push({
field: '',
unit: '',
type: ''
});
}
removeCurrent(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.fields.length > 1) {
this.fields.splice(index, 1);
}
}
onChange() {
this.value = this.fields;
this.valueChange.emit(JSON.stringify(this.value));
}
}
@@ -1,5 +1,8 @@
:host {
::ng-deep {
.ant-input-affix-wrapper {
width: 100%!important;
}
.ant-input-prefix, .ant-input-suffix {
display: flex;
gap: 4px;
+1 -1
View File
@@ -18,5 +18,5 @@
*/
export const CONSTANTS = {
VERSION: 'v1.6.1'
VERSION: 'v1.7.0'
};

Some files were not shown because too many files have changed in this diff Show More