mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
[feature] add plugin management and support plugin hot reloading (#2238)
Signed-off-by: liutianyou <tianyou.liu@qq.com> Co-authored-by: Logic <zqr10159@dromara.org>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.common.constants;
|
||||
|
||||
/**
|
||||
* plugin type
|
||||
*/
|
||||
public enum PluginType {
|
||||
|
||||
/**
|
||||
* do something after alter
|
||||
*/
|
||||
POST_ALERT
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.dto;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* data transfer class for uploading plugins
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class PluginUpload {
|
||||
|
||||
@NotNull
|
||||
private MultipartFile jarFile;
|
||||
|
||||
@NotNull(message = "Plugin name is required")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "Enable status is required")
|
||||
private Boolean enableStatus;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.common.entity.manager;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.Objects;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.hertzbeat.common.constants.PluginType;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
/**
|
||||
* Plugin Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "hzb_plugin_item")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "PluginItem Entity")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class PluginItem {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Schema(title = "Plugin Primary key index ID", example = "87584674384", accessMode = READ_ONLY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "metadata_id")
|
||||
@JsonIgnore
|
||||
private PluginMetadata pluginMetadata;
|
||||
|
||||
@Schema(title = "Plugin implementation class full path", example = "org.apache.hertzbeat.plugin.impl.DemoPluginImpl", accessMode = READ_WRITE)
|
||||
private String classIdentifier;
|
||||
|
||||
@Schema(title = "Plugin type", example = "POST_ALERT", accessMode = READ_WRITE)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private PluginType type;
|
||||
|
||||
public PluginItem(String classIdentifier, PluginType type) {
|
||||
this.classIdentifier = classIdentifier;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
PluginItem that = (PluginItem) o;
|
||||
return Objects.equals(id, that.id) && Objects.equals(classIdentifier, that.classIdentifier) && type == that.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, classIdentifier, type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.common.entity.manager;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
/**
|
||||
* Plugin Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "hzb_plugin_metadata")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "Plugin Entity")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class PluginMetadata {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Schema(title = "Plugin Primary key index ID", example = "87584674384", accessMode = READ_ONLY)
|
||||
private Long id;
|
||||
|
||||
@Schema(title = "plugin name", example = "notification plugin", accessMode = READ_WRITE)
|
||||
@NotNull
|
||||
private String name;
|
||||
|
||||
@Schema(title = "Plugin activation status", example = "true", accessMode = READ_WRITE)
|
||||
private Boolean enableStatus;
|
||||
|
||||
@Schema(title = "Jar file path", example = "true", accessMode = READ_WRITE)
|
||||
private String jarFilePath;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
PluginMetadata that = (PluginMetadata) o;
|
||||
return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(enableStatus, that.enableStatus) && Objects.equals(jarFilePath,
|
||||
that.jarFilePath) && Objects.equals(creator, that.creator) && Objects.equals(gmtCreate, that.gmtCreate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, name, enableStatus, jarFilePath, creator, gmtCreate);
|
||||
}
|
||||
|
||||
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
|
||||
@CreatedBy
|
||||
private String creator;
|
||||
|
||||
@Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY)
|
||||
@CreatedDate
|
||||
private LocalDateTime gmtCreate;
|
||||
|
||||
@OneToMany(targetEntity = PluginItem.class, cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "metadata_id", referencedColumnName = "id")
|
||||
private List<PluginItem> items;
|
||||
|
||||
}
|
||||
+18
-18
@@ -21,7 +21,6 @@ import com.google.common.collect.Maps;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.AlerterWorkerPool;
|
||||
import org.apache.hertzbeat.common.entity.alerter.Alert;
|
||||
@@ -30,6 +29,7 @@ import org.apache.hertzbeat.common.entity.manager.NoticeRule;
|
||||
import org.apache.hertzbeat.common.entity.manager.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.apache.hertzbeat.manager.service.NoticeConfigService;
|
||||
import org.apache.hertzbeat.manager.service.PluginService;
|
||||
import org.apache.hertzbeat.manager.support.exception.AlertNoticeException;
|
||||
import org.apache.hertzbeat.manager.support.exception.IgnoreException;
|
||||
import org.apache.hertzbeat.plugin.Plugin;
|
||||
@@ -42,6 +42,7 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
@Slf4j
|
||||
public class DispatcherAlarm implements InitializingBean {
|
||||
|
||||
private static final int DISPATCH_THREADS = 3;
|
||||
|
||||
private final AlerterWorkerPool workerPool;
|
||||
@@ -49,16 +50,18 @@ public class DispatcherAlarm implements InitializingBean {
|
||||
private final NoticeConfigService noticeConfigService;
|
||||
private final AlertStoreHandler alertStoreHandler;
|
||||
private final Map<Byte, AlertNotifyHandler> alertNotifyHandlerMap;
|
||||
private final PluginService pluginService;
|
||||
|
||||
public DispatcherAlarm(AlerterWorkerPool workerPool,
|
||||
CommonDataQueue dataQueue,
|
||||
NoticeConfigService noticeConfigService,
|
||||
AlertStoreHandler alertStoreHandler,
|
||||
List<AlertNotifyHandler> alertNotifyHandlerList) {
|
||||
CommonDataQueue dataQueue,
|
||||
NoticeConfigService noticeConfigService,
|
||||
AlertStoreHandler alertStoreHandler,
|
||||
List<AlertNotifyHandler> alertNotifyHandlerList, PluginService pluginService) {
|
||||
this.workerPool = workerPool;
|
||||
this.dataQueue = dataQueue;
|
||||
this.noticeConfigService = noticeConfigService;
|
||||
this.alertStoreHandler = alertStoreHandler;
|
||||
this.pluginService = pluginService;
|
||||
alertNotifyHandlerMap = Maps.newHashMapWithExpectedSize(alertNotifyHandlerList.size());
|
||||
alertNotifyHandlerList.forEach(r -> alertNotifyHandlerMap.put(r.type(), r));
|
||||
}
|
||||
@@ -127,11 +130,8 @@ public class DispatcherAlarm implements InitializingBean {
|
||||
alertStoreHandler.store(alert);
|
||||
// Notice distribution
|
||||
sendNotify(alert);
|
||||
// Execute the plugin
|
||||
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class, Plugin.class.getClassLoader());
|
||||
for (Plugin plugin : loader) {
|
||||
plugin.alert(alert);
|
||||
}
|
||||
// Execute the plugin if enable
|
||||
pluginService.pluginExecute(Plugin.class, plugin -> plugin.alert(alert));
|
||||
}
|
||||
} catch (IgnoreException ignored) {
|
||||
} catch (InterruptedException e) {
|
||||
@@ -147,14 +147,14 @@ public class DispatcherAlarm implements InitializingBean {
|
||||
noticeRules.forEach(rule -> {
|
||||
workerPool.executeNotify(() -> {
|
||||
rule.getReceiverId()
|
||||
.forEach(receiverId -> {
|
||||
try {
|
||||
sendNoticeMsg(getOneReceiverById(receiverId),
|
||||
getOneTemplateById(rule.getTemplateId()), alert);
|
||||
} catch (AlertNoticeException e) {
|
||||
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
.forEach(receiverId -> {
|
||||
try {
|
||||
sendNoticeMsg(getOneReceiverById(receiverId),
|
||||
getOneTemplateById(rule.getTemplateId()), alert);
|
||||
} catch (AlertNoticeException e) {
|
||||
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.controller;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.common.entity.dto.PluginUpload;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginMetadata;
|
||||
import org.apache.hertzbeat.manager.service.PluginService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* plugin management API
|
||||
*/
|
||||
@io.swagger.v3.oas.annotations.tags.Tag(name = "Plugin Manage API")
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/plugin", produces = {APPLICATION_JSON_VALUE})
|
||||
@RequiredArgsConstructor
|
||||
public class PluginController {
|
||||
|
||||
private final PluginService pluginService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "upload plugin", description = "upload plugin")
|
||||
public ResponseEntity<Message<Void>> uploadNewPlugin(@Valid PluginUpload pluginUpload) {
|
||||
pluginService.savePlugin(pluginUpload);
|
||||
return ResponseEntity.ok(Message.success("Add success"));
|
||||
}
|
||||
|
||||
|
||||
@GetMapping()
|
||||
@Operation(summary = "Get Plugins information", description = "Obtain plugins information based on conditions")
|
||||
public ResponseEntity<Message<Page<PluginMetadata>>> getPlugins(
|
||||
@Parameter(description = "plugin name search", example = "status") @RequestParam(required = false) String search,
|
||||
@Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex,
|
||||
@Parameter(description = "Number of list pagination", example = "8") @RequestParam(defaultValue = "8") int pageSize) {
|
||||
// Get tag information
|
||||
Specification<PluginMetadata> specification = (root, query, criteriaBuilder) -> {
|
||||
List<Predicate> andList = new ArrayList<>();
|
||||
if (search != null && !search.isEmpty()) {
|
||||
Predicate predicateApp = criteriaBuilder.like(root.get("name"), "%" + search + "%");
|
||||
andList.add(predicateApp);
|
||||
}
|
||||
Predicate[] andPredicates = new Predicate[andList.size()];
|
||||
Predicate andPredicate = criteriaBuilder.and(andList.toArray(andPredicates));
|
||||
|
||||
if (andPredicates.length == 0) {
|
||||
return query.where().getRestriction();
|
||||
} else {
|
||||
return andPredicate;
|
||||
}
|
||||
};
|
||||
PageRequest pageRequest = PageRequest.of(pageIndex, pageSize);
|
||||
Page<PluginMetadata> alertPage = pluginService.getPlugins(specification, pageRequest);
|
||||
Message<Page<PluginMetadata>> message = Message.success(alertPage);
|
||||
return ResponseEntity.ok(message);
|
||||
}
|
||||
|
||||
@DeleteMapping()
|
||||
@Operation(summary = "Delete plugins based on ID", description = "Delete plugins based on ID")
|
||||
public ResponseEntity<Message<Void>> deleteTags(
|
||||
@Parameter(description = "Plugin IDs ", example = "6565463543") @RequestParam(required = false) List<Long> ids) {
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
pluginService.deletePlugins(new HashSet<>(ids));
|
||||
}
|
||||
return ResponseEntity.ok(Message.success("Delete success"));
|
||||
}
|
||||
|
||||
|
||||
@PutMapping()
|
||||
@Operation(summary = "Update enable status", description = "Delete plugins based on ID")
|
||||
public ResponseEntity<Message<Void>> updatePluginStatus(@RequestBody PluginMetadata plugin) {
|
||||
pluginService.updateStatus(plugin);
|
||||
return ResponseEntity.ok(Message.success("Update success"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.dao;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
/**
|
||||
* plugin metadata repository
|
||||
*/
|
||||
public interface PluginItemDao extends JpaRepository<PluginItem, Long>, JpaSpecificationExecutor<PluginItem> {
|
||||
|
||||
int countPluginItemByClassIdentifierIn(List<String> classIdentifiers);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.dao;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginMetadata;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
/**
|
||||
* plugin metadata repository
|
||||
*/
|
||||
public interface PluginMetadataDao extends JpaRepository<PluginMetadata, Long>, JpaSpecificationExecutor<PluginMetadata> {
|
||||
|
||||
/**
|
||||
* count by name
|
||||
*
|
||||
* @param name name
|
||||
* @return count
|
||||
*/
|
||||
int countPluginMetadataByName(String name);
|
||||
|
||||
|
||||
/**
|
||||
* find enabled plugins
|
||||
* @return plugins
|
||||
*/
|
||||
List<PluginMetadata> findPluginMetadataByEnableStatusTrue();
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.service;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.hertzbeat.common.entity.dto.PluginUpload;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginMetadata;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
/**
|
||||
* plugin service
|
||||
*/
|
||||
public interface PluginService {
|
||||
|
||||
/**
|
||||
* save plugin
|
||||
*/
|
||||
void savePlugin(PluginUpload pluginUpload);
|
||||
|
||||
/**
|
||||
* Determine whether the plugin is enabled
|
||||
*
|
||||
* @param clazz plugin Class
|
||||
* @return return true if enabled
|
||||
*/
|
||||
boolean pluginIsEnable(Class<?> clazz);
|
||||
|
||||
|
||||
/**
|
||||
* get plugin page list
|
||||
*
|
||||
* @param specification Query condition
|
||||
* @param pageRequest Paging condition
|
||||
* @return Plugins
|
||||
*/
|
||||
Page<PluginMetadata> getPlugins(Specification<PluginMetadata> specification, PageRequest pageRequest);
|
||||
|
||||
/**
|
||||
* execute plugin
|
||||
* @param clazz plugin interface
|
||||
* @param execute run plugin logic
|
||||
* @param <T> plugin type
|
||||
*/
|
||||
<T> void pluginExecute(Class<T> clazz, Consumer<T> execute);
|
||||
|
||||
/**
|
||||
* delete plugin
|
||||
*
|
||||
* @param ids set of plugin id
|
||||
*/
|
||||
void deletePlugins(Set<Long> ids);
|
||||
|
||||
void updateStatus(PluginMetadata plugin);
|
||||
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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.service.impl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.hertzbeat.common.constants.PluginType;
|
||||
import org.apache.hertzbeat.common.entity.dto.PluginUpload;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginItem;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginMetadata;
|
||||
import org.apache.hertzbeat.common.support.exception.CommonException;
|
||||
import org.apache.hertzbeat.manager.dao.PluginItemDao;
|
||||
import org.apache.hertzbeat.manager.dao.PluginMetadataDao;
|
||||
import org.apache.hertzbeat.manager.service.PluginService;
|
||||
import org.apache.hertzbeat.plugin.Plugin;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* plugin service
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PluginServiceImpl implements PluginService {
|
||||
|
||||
private final PluginMetadataDao metadataDao;
|
||||
|
||||
private final PluginItemDao itemDao;
|
||||
|
||||
public static Map<Class<?>, PluginType> PLUGIN_TYPE_MAPPING = new HashMap<>();
|
||||
|
||||
/**
|
||||
* plugin status
|
||||
*/
|
||||
private static final Map<String, Boolean> PLUGIN_ENABLE_STATUS = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
private URLClassLoader pluginClassLoader;
|
||||
|
||||
@Override
|
||||
public void deletePlugins(Set<Long> ids) {
|
||||
List<PluginMetadata> plugins = metadataDao.findAllById(ids);
|
||||
for (PluginMetadata plugin : plugins) {
|
||||
try {
|
||||
// delete jar file
|
||||
File jarFile = new File(plugin.getJarFilePath());
|
||||
if (jarFile.exists()) {
|
||||
FileUtils.delete(jarFile);
|
||||
}
|
||||
// delete metadata
|
||||
metadataDao.deleteById(plugin.getId());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
syncPluginStatus();
|
||||
// reload classloader
|
||||
loadJarToClassLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(PluginMetadata plugin) {
|
||||
Optional<PluginMetadata> pluginMetadata = metadataDao.findById(plugin.getId());
|
||||
if (pluginMetadata.isPresent()) {
|
||||
PluginMetadata metadata = pluginMetadata.get();
|
||||
metadata.setEnableStatus(plugin.getEnableStatus());
|
||||
metadataDao.save(metadata);
|
||||
syncPluginStatus();
|
||||
} else {
|
||||
throw new IllegalArgumentException("The plugin is not existed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static {
|
||||
PLUGIN_TYPE_MAPPING.put(Plugin.class, PluginType.POST_ALERT);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* verify the type of the jar package
|
||||
*
|
||||
* @param jarFile jar file
|
||||
* @return return the full path of the Plugin interface implementation class
|
||||
*/
|
||||
public List<PluginItem> validateJarFile(File jarFile) {
|
||||
List<PluginItem> pluginItems = new ArrayList<>();
|
||||
try {
|
||||
URL jarUrl = new URL("file:" + jarFile.getAbsolutePath());
|
||||
try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, this.getClass().getClassLoader());
|
||||
JarFile jar = new JarFile(jarFile)) {
|
||||
Enumeration<JarEntry> entries = jar.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
if (entry.getName().endsWith(".class")) {
|
||||
String className = entry.getName().replace("/", ".").replace(".class", "");
|
||||
try {
|
||||
Class<?> cls = classLoader.loadClass(className);
|
||||
if (!cls.isInterface()) {
|
||||
PLUGIN_TYPE_MAPPING.forEach((clazz, type) -> {
|
||||
if (clazz.isAssignableFrom(cls)) {
|
||||
pluginItems.add(new PluginItem(className, type));
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
System.err.println("Failed to load class: " + className);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pluginItems.isEmpty()) {
|
||||
throw new CommonException("Illegal plug-ins, please refer to https://hertzbeat.apache.org/docs/help/plugin/");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading JAR file:{}", jarFile.getAbsoluteFile(), e);
|
||||
throw new CommonException("Error reading JAR file: " + jarFile.getAbsolutePath());
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
log.error("Invalid JAR file URL: {}", jarFile.getAbsoluteFile(), e);
|
||||
throw new CommonException("Invalid JAR file URL: " + jarFile.getAbsolutePath());
|
||||
}
|
||||
return pluginItems;
|
||||
}
|
||||
|
||||
private void validateMetadata(PluginMetadata metadata) {
|
||||
if (metadataDao.countPluginMetadataByName(metadata.getName()) != 0) {
|
||||
throw new CommonException("A plugin named " + metadata.getName() + " already exists");
|
||||
}
|
||||
if (itemDao.countPluginItemByClassIdentifierIn((metadata.getItems().stream().map(PluginItem::getClassIdentifier).collect(Collectors.toList()))) != 0) {
|
||||
throw new CommonException("Plugin already exists");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void savePlugin(PluginUpload pluginUpload) {
|
||||
String jarPath = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()).getAbsolutePath();
|
||||
Path extLibPath = Paths.get(new File(jarPath).getParent(), "plugin-lib");
|
||||
File extLibDir = extLibPath.toFile();
|
||||
|
||||
String fileName = pluginUpload.getJarFile().getOriginalFilename();
|
||||
if (fileName == null) {
|
||||
throw new CommonException("Failed to upload plugin");
|
||||
}
|
||||
File destFile = new File(extLibDir, fileName);
|
||||
FileUtils.createParentDirectories(destFile);
|
||||
pluginUpload.getJarFile().transferTo(destFile);
|
||||
List<PluginItem> pluginItems = validateJarFile(destFile);
|
||||
|
||||
// save plugin metadata
|
||||
PluginMetadata pluginMetadata = PluginMetadata.builder()
|
||||
.name(pluginUpload.getName())
|
||||
.enableStatus(true)
|
||||
.items(pluginItems).jarFilePath(destFile.getAbsolutePath())
|
||||
.gmtCreate(LocalDateTime.now())
|
||||
.build();
|
||||
validateMetadata(pluginMetadata);
|
||||
metadataDao.save(pluginMetadata);
|
||||
itemDao.saveAll(pluginItems);
|
||||
// load jar to classloader
|
||||
loadJarToClassLoader();
|
||||
// sync enabled status
|
||||
syncPluginStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pluginIsEnable(Class<?> clazz) {
|
||||
return Boolean.TRUE.equals(PLUGIN_ENABLE_STATUS.get(clazz.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<PluginMetadata> getPlugins(Specification<PluginMetadata> specification, PageRequest pageRequest) {
|
||||
return metadataDao.findAll(specification, pageRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all plugin enabled states into memory
|
||||
*/
|
||||
@PostConstruct
|
||||
private void syncPluginStatus() {
|
||||
List<PluginMetadata> plugins = metadataDao.findAll();
|
||||
Map<String, Boolean> statusMap = new HashMap<>();
|
||||
for (PluginMetadata plugin : plugins) {
|
||||
for (PluginItem item : plugin.getItems()) {
|
||||
statusMap.put(item.getClassIdentifier(), plugin.getEnableStatus());
|
||||
}
|
||||
}
|
||||
PLUGIN_ENABLE_STATUS.clear();
|
||||
PLUGIN_ENABLE_STATUS.putAll(statusMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* load jar to classloader
|
||||
*/
|
||||
@PostConstruct
|
||||
private void loadJarToClassLoader() {
|
||||
try {
|
||||
if (pluginClassLoader != null) {
|
||||
pluginClassLoader.close();
|
||||
}
|
||||
List<PluginMetadata> plugins = metadataDao.findPluginMetadataByEnableStatusTrue();
|
||||
List<URL> urls = new ArrayList<>();
|
||||
for (PluginMetadata metadata : plugins) {
|
||||
URL url = new File(metadata.getJarFilePath()).toURI().toURL();
|
||||
urls.add(url);
|
||||
}
|
||||
pluginClassLoader = new URLClassLoader(urls.toArray(new URL[0]));
|
||||
} catch (MalformedURLException e) {
|
||||
log.error("Failed to load plugin:{}", e.getMessage());
|
||||
throw new CommonException("Failed to load plugin:" + e.getMessage());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void pluginExecute(Class<T> clazz, Consumer<T> execute) {
|
||||
ServiceLoader<T> load = ServiceLoader.load(clazz, pluginClassLoader);
|
||||
for (T t : load) {
|
||||
if (pluginIsEnable(t.getClass())) {
|
||||
execute.accept(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.controller;
|
||||
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginMetadata;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.manager.service.PluginService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
/**
|
||||
* test case for plugin controller
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@InjectMocks
|
||||
private PluginController pluginController;
|
||||
|
||||
@Mock
|
||||
private PluginService pluginService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.mockMvc = MockMvcBuilders.standaloneSetup(pluginController).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadNewPlugin() throws Exception {
|
||||
MockMultipartFile jarFile = new MockMultipartFile(
|
||||
"jarFile",
|
||||
"plugin-test.jar",
|
||||
"application/java-archive",
|
||||
"This is the file content".getBytes()
|
||||
);
|
||||
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.multipart("/api/plugin")
|
||||
.file(jarFile)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.param("name", "test-plugin")
|
||||
.param("enableStatus", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Add success"))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPlugins() throws Exception {
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/plugin?&search={search}", "test"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteTags() throws Exception {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
ids.add(6565463543L);
|
||||
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/plugin")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonUtil.toJson(ids)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Delete success"))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePluginStatus() throws Exception {
|
||||
PluginMetadata metadata = new PluginMetadata();
|
||||
metadata.setId(6565463543L);
|
||||
metadata.setEnableStatus(true);
|
||||
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.put("/api/plugin")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonUtil.toJson(metadata)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Update success"))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.apache.hertzbeat.common.constants.PluginType;
|
||||
import org.apache.hertzbeat.common.entity.dto.PluginUpload;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginItem;
|
||||
import org.apache.hertzbeat.common.entity.manager.PluginMetadata;
|
||||
import org.apache.hertzbeat.manager.dao.PluginItemDao;
|
||||
import org.apache.hertzbeat.manager.dao.PluginMetadataDao;
|
||||
import org.apache.hertzbeat.manager.service.impl.PluginServiceImpl;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
/**
|
||||
* Test case for {@link PluginService}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginServiceTest {
|
||||
|
||||
@InjectMocks
|
||||
private PluginServiceImpl pluginService;
|
||||
@Mock
|
||||
private PluginMetadataDao metadataDao;
|
||||
|
||||
@Mock
|
||||
private PluginItemDao itemDao;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
pluginService = new PluginServiceImpl(metadataDao, itemDao);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSavePlugin(){
|
||||
|
||||
List<PluginItem> pluginItems = Collections.singletonList(new PluginItem("org.apache.hertzbear.PluginTest", PluginType.POST_ALERT));
|
||||
PluginServiceImpl service = spy(pluginService);
|
||||
doReturn(pluginItems).when(service).validateJarFile(any());
|
||||
|
||||
MockMultipartFile mockFile = new MockMultipartFile("file", "test-plugin.jar", "application/java-archive", "plugin-content".getBytes());
|
||||
PluginUpload pluginUpload = new PluginUpload(mockFile, "Test Plugin", true);
|
||||
|
||||
when(metadataDao.save(any(PluginMetadata.class))).thenReturn(new PluginMetadata());
|
||||
when(itemDao.saveAll(anyList())).thenReturn(Collections.emptyList());
|
||||
|
||||
service.savePlugin(pluginUpload);
|
||||
verify(metadataDao, times(1)).save(any(PluginMetadata.class));
|
||||
verify(itemDao, times(1)).saveAll(anyList());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateStatus() {
|
||||
PluginMetadata plugin = new PluginMetadata();
|
||||
plugin.setId(1L);
|
||||
plugin.setEnableStatus(true);
|
||||
plugin.setName("test-plugin");
|
||||
|
||||
when(metadataDao.findById(1L)).thenReturn(Optional.of(plugin));
|
||||
when(metadataDao.save(any(PluginMetadata.class))).thenReturn(plugin);
|
||||
assertDoesNotThrow(() -> pluginService.updateStatus(plugin));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeletePlugins() {
|
||||
PluginMetadata plugin = new PluginMetadata();
|
||||
plugin.setId(1L);
|
||||
plugin.setJarFilePath("path/to/plugin.jar");
|
||||
Set<Long> ids = new HashSet<>(Collections.singletonList(1L));
|
||||
|
||||
when(metadataDao.findAllById(ids)).thenReturn(Collections.singletonList(plugin));
|
||||
doNothing().when(metadataDao).deleteById(anyLong());
|
||||
|
||||
pluginService.deletePlugins(ids);
|
||||
verify(metadataDao, times(1)).deleteById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetPlugins() {
|
||||
Specification<PluginMetadata> spec = mock(Specification.class);
|
||||
PageRequest pageRequest = PageRequest.of(0, 10);
|
||||
Page<PluginMetadata> page = new PageImpl<>(Collections.singletonList(new PluginMetadata()));
|
||||
|
||||
when(metadataDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(page);
|
||||
|
||||
Page<PluginMetadata> result = pluginService.getPlugins(spec, pageRequest);
|
||||
assertFalse(result.isEmpty());
|
||||
verify(metadataDao, times(1)).findAll(any(Specification.class), any(PageRequest.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 { PluginItem } from './PluginItem';
|
||||
|
||||
export class Plugin {
|
||||
id!: number;
|
||||
name!: string;
|
||||
enableStatus!: boolean;
|
||||
items!: PluginItem[];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export class PluginItem {
|
||||
type!: string;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<app-help-message-show
|
||||
[help_message_content]="'plugin.help' | i18n"
|
||||
[guild_link]="'plugin.help.link' | i18n"
|
||||
[module_name]="'menu.advanced.plugins'"
|
||||
[icon_name]="'usb'"
|
||||
></app-help-message-show>
|
||||
|
||||
<nz-divider></nz-divider>
|
||||
|
||||
<app-toolbar>
|
||||
<ng-template #left>
|
||||
<button nz-button nzType="primary" nz-tooltip (click)="sync()" [nzTooltipTitle]="'common.refresh' | i18n">
|
||||
<i nz-icon nzType="sync" nzTheme="outline"></i>
|
||||
</button>
|
||||
<button nz-button nzType="primary" (click)="onNewPlugin()">
|
||||
<i nz-icon nzType="appstore-add" nzTheme="outline"></i>
|
||||
{{ 'plugin.upload' | i18n }}
|
||||
</button>
|
||||
|
||||
<button nz-button nzType="primary" nzDanger (click)="onDeletePlugins()">
|
||||
<i nz-icon nzType="delete" nzTheme="outline"></i>
|
||||
{{ 'plugin.delete' | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #right>
|
||||
<app-multi-func-input
|
||||
class="mobile-hide"
|
||||
groupStyle="width: 250px;"
|
||||
[placeholder]="'plugin.search' | i18n"
|
||||
[(value)]="search"
|
||||
(valueChange)="loadPluginsTable()"
|
||||
/>
|
||||
</ng-template>
|
||||
</app-toolbar>
|
||||
|
||||
<nz-table
|
||||
#fixedTable
|
||||
[nzData]="plugins"
|
||||
[nzPageIndex]="pageIndex"
|
||||
[nzPageSize]="pageSize"
|
||||
[nzTotal]="total"
|
||||
nzFrontPagination="false"
|
||||
[nzLoading]="tableLoading"
|
||||
nzShowSizeChanger
|
||||
[nzShowTotal]="rangeTemplate"
|
||||
[nzPageSizeOptions]="[8, 15, 25]"
|
||||
(nzQueryParams)="onTablePageChange($event)"
|
||||
nzShowPagination="true"
|
||||
[nzScroll]="{ x: '1240px' }"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th nzAlign="center" nzLeft nzWidth="3%" [(nzChecked)]="checkedAll" (nzCheckedChange)="onAllChecked($event)"></th>
|
||||
<th nzAlign="center" nzWidth="15%">{{ 'plugin.name' | i18n }}</th>
|
||||
<th nzAlign="center" nzWidth="15%">{{ 'plugin.type' | i18n }}</th>
|
||||
<th nzAlign="center" nzWidth="15%">{{ 'plugin.status' | i18n }}</th>
|
||||
<th nzAlign="center" nzWidth="17%">{{ 'common.edit' | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let data of fixedTable.data">
|
||||
<td nzAlign="center" nzLeft [nzChecked]="checkedTagIds.has(data.id)" (nzCheckedChange)="onItemChecked(data.id, $event)"></td>
|
||||
<td nzAlign="center">{{ data.name }}</td>
|
||||
<td nzAlign="left">
|
||||
<nz-tag *ngFor="let item of data?.items; let i = index" style="margin-top: 2px" class="hoverClass">
|
||||
{{ 'plugin.type.' + item.type | i18n }}
|
||||
</nz-tag>
|
||||
</td>
|
||||
|
||||
<td nzAlign="center">
|
||||
<nz-switch [(ngModel)]="data.enableStatus" (ngModelChange)="updatePluginEnableStatus(data)" name="enable"></nz-switch>
|
||||
</td>
|
||||
<!-- <td nzAlign="center">{{ data.enableStatus }}</td>-->
|
||||
<td nzAlign="center">
|
||||
<div class="actions">
|
||||
<button
|
||||
nz-button
|
||||
nzType="primary"
|
||||
nzDanger
|
||||
(click)="onDeleteOnePlugin(data.id)"
|
||||
nz-tooltip
|
||||
[nzTooltipTitle]="'plugin.delete' | i18n"
|
||||
>
|
||||
<i nz-icon nzType="delete" nzTheme="outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</nz-table>
|
||||
|
||||
<ng-template #rangeTemplate> {{ 'common.total' | i18n }} {{ total }} </ng-template>
|
||||
|
||||
<!-- upload plugin pop-up window -->
|
||||
<nz-modal
|
||||
[(nzVisible)]="isManageModalVisible"
|
||||
[nzTitle]="isManageModalAdd ? ('plugin.upload' | i18n) : ('plugin.edit' | i18n)"
|
||||
(nzOnCancel)="onManageModalCancel()"
|
||||
(nzOnOk)="onManageModalOk()"
|
||||
nzMaskClosable="false"
|
||||
nzWidth="30%"
|
||||
[nzOkLoading]="isManageModalOkLoading"
|
||||
>
|
||||
<div *nzModalContent class="-inner-content">
|
||||
<form [formGroup]="pluginForm" nz-form>
|
||||
<nz-form-item>
|
||||
<nz-form-label [nzSpan]="6" nzRequired="true" nzFor="name">{{ 'plugin.name' | i18n }}</nz-form-label>
|
||||
<nz-form-control [nzSpan]="14" nzErrorTip="Please input the name">
|
||||
<input nz-input formControlName="name" id="name" />
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
|
||||
<nz-form-item>
|
||||
<nz-form-label [nzSpan]="6" nzRequired="true" nzFor="jarFile">{{ 'plugin.jar.file' | i18n }}</nz-form-label>
|
||||
<nz-form-control [nzSpan]="14" nzErrorTip="Please upload a file">
|
||||
<nz-upload nzAccept=".jar" nzAction="" nzListType="text" [(nzFileList)]="fileList" [nzBeforeUpload]="beforeUpload">
|
||||
<button nz-button>
|
||||
<i nz-icon nzType="upload"></i>
|
||||
{{ 'common.file.select' | i18n }}
|
||||
</button>
|
||||
</nz-upload>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
|
||||
<nz-form-item>
|
||||
<nz-form-label [nzSpan]="6" nzRequired="true" nzFor="enableStatus">{{ 'plugin.status' | i18n }}</nz-form-label>
|
||||
<nz-form-control [nzSpan]="14">
|
||||
<nz-switch formControlName="enableStatus" required></nz-switch>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</form>
|
||||
</div>
|
||||
</nz-modal>
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SettingPluginsComponent } from './plugin.component';
|
||||
|
||||
describe('SettingPluginsComponent', () => {
|
||||
let component: SettingPluginsComponent;
|
||||
let fixture: ComponentFixture<SettingPluginsComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SettingPluginsComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SettingPluginsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { I18NService } from '@core';
|
||||
import { ALAIN_I18N_TOKEN } from '@delon/theme';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
import { NzNotificationService } from 'ng-zorro-antd/notification';
|
||||
import { NzTableQueryParams } from 'ng-zorro-antd/table';
|
||||
import { NzUploadFile } from 'ng-zorro-antd/upload';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
import { Plugin } from '../../../pojo/Plugin';
|
||||
import { PluginService } from '../../../service/plugin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-setting-plugins',
|
||||
templateUrl: './plugin.component.html'
|
||||
})
|
||||
export class SettingPluginsComponent implements OnInit {
|
||||
constructor(
|
||||
private notifySvc: NzNotificationService,
|
||||
private modal: NzModalService,
|
||||
private pluginService: PluginService,
|
||||
private fb: FormBuilder,
|
||||
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
|
||||
) {
|
||||
this.pluginForm = this.fb.group({
|
||||
name: [null, [Validators.required]],
|
||||
jarFile: [null, [Validators.required]],
|
||||
enableStatus: [true, [Validators.required]]
|
||||
});
|
||||
}
|
||||
|
||||
pageIndex: number = 1;
|
||||
pageSize: number = 8;
|
||||
total: number = 0;
|
||||
plugins!: Plugin[];
|
||||
tableLoading: boolean = false;
|
||||
checkedTagIds = new Set<number>();
|
||||
// search by name
|
||||
search: string | undefined;
|
||||
fileList: NzUploadFile[] = [];
|
||||
pluginForm: FormGroup;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadPluginsTable();
|
||||
}
|
||||
|
||||
sync() {
|
||||
this.loadPluginsTable();
|
||||
}
|
||||
|
||||
beforeUpload = (file: NzUploadFile): boolean => {
|
||||
this.fileList = [file];
|
||||
this.pluginForm.patchValue({
|
||||
jarFile: file
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
loadPluginsTable() {
|
||||
this.tableLoading = true;
|
||||
let pluginsInit$ = this.pluginService.loadPlugins(this.search, 1, this.pageIndex - 1, this.pageSize).subscribe(
|
||||
message => {
|
||||
this.tableLoading = false;
|
||||
this.checkedAll = false;
|
||||
this.checkedTagIds.clear();
|
||||
if (message.code === 0) {
|
||||
let page = message.data;
|
||||
this.plugins = page.content;
|
||||
this.pageIndex = page.number + 1;
|
||||
this.total = page.totalElements;
|
||||
} else {
|
||||
console.warn(message.msg);
|
||||
}
|
||||
pluginsInit$.unsubscribe();
|
||||
},
|
||||
error => {
|
||||
this.tableLoading = false;
|
||||
pluginsInit$.unsubscribe();
|
||||
console.error(error.msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updatePluginEnableStatus(plugin: Plugin) {
|
||||
plugin.enableStatus = !plugin.enableStatus;
|
||||
this.tableLoading = true;
|
||||
const updateDefine$ = this.pluginService
|
||||
.updatePluginStatus(plugin)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
updateDefine$.unsubscribe();
|
||||
this.tableLoading = false;
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
message => {
|
||||
if (message.code === 0) {
|
||||
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.edit-success'), '');
|
||||
} else {
|
||||
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), message.msg);
|
||||
}
|
||||
this.loadPluginsTable();
|
||||
this.tableLoading = false;
|
||||
},
|
||||
error => {
|
||||
this.tableLoading = false;
|
||||
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), error.msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onDeletePlugins() {
|
||||
if (this.checkedTagIds == null || this.checkedTagIds.size === 0) {
|
||||
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), '');
|
||||
return;
|
||||
}
|
||||
this.modal.confirm({
|
||||
nzTitle: this.i18nSvc.fanyi('common.confirm.delete-batch'),
|
||||
nzOkText: this.i18nSvc.fanyi('common.button.ok'),
|
||||
nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
|
||||
nzOkDanger: true,
|
||||
nzOkType: 'primary',
|
||||
nzClosable: false,
|
||||
nzOnOk: () => this.deletePlugins(this.checkedTagIds)
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteOnePlugin(pluginId: number) {
|
||||
let alerts = new Set<number>();
|
||||
alerts.add(pluginId);
|
||||
this.modal.confirm({
|
||||
nzTitle: this.i18nSvc.fanyi('common.confirm.delete'),
|
||||
nzOkText: this.i18nSvc.fanyi('common.button.ok'),
|
||||
nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
|
||||
nzOkDanger: true,
|
||||
nzOkType: 'primary',
|
||||
nzClosable: false,
|
||||
nzOnOk: () => this.deletePlugins(alerts)
|
||||
});
|
||||
}
|
||||
|
||||
deletePlugins(pluginIds: Set<number>) {
|
||||
this.tableLoading = true;
|
||||
const deleteTags$ = this.pluginService.deletePlugins(pluginIds).subscribe(
|
||||
message => {
|
||||
deleteTags$.unsubscribe();
|
||||
if (message.code === 0) {
|
||||
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
|
||||
this.updatePageIndex(pluginIds.size);
|
||||
this.loadPluginsTable();
|
||||
} else {
|
||||
this.tableLoading = false;
|
||||
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), message.msg);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.tableLoading = false;
|
||||
deleteTags$.unsubscribe();
|
||||
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), error.msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updatePageIndex(delSize: number) {
|
||||
const lastPage = Math.max(1, Math.ceil((this.total - delSize) / this.pageSize));
|
||||
this.pageIndex = this.pageIndex > lastPage ? lastPage : this.pageIndex;
|
||||
}
|
||||
|
||||
// begin: 列表多选分页逻辑
|
||||
checkedAll: boolean = false;
|
||||
|
||||
onAllChecked(checked: boolean) {
|
||||
if (checked) {
|
||||
this.plugins.forEach(monitor => this.checkedTagIds.add(monitor.id));
|
||||
} else {
|
||||
this.checkedTagIds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
onItemChecked(monitorId: number, checked: boolean) {
|
||||
if (checked) {
|
||||
this.checkedTagIds.add(monitorId);
|
||||
} else {
|
||||
this.checkedTagIds.delete(monitorId);
|
||||
}
|
||||
}
|
||||
|
||||
onTablePageChange(params: NzTableQueryParams) {
|
||||
const { pageSize, pageIndex, sort, filter } = params;
|
||||
this.pageIndex = pageIndex;
|
||||
this.pageSize = pageSize;
|
||||
this.loadPluginsTable();
|
||||
}
|
||||
|
||||
isManageModalVisible = false;
|
||||
isManageModalOkLoading = false;
|
||||
isManageModalAdd = true;
|
||||
|
||||
onNewPlugin() {
|
||||
this.isManageModalVisible = true;
|
||||
this.isManageModalAdd = true;
|
||||
}
|
||||
|
||||
onManageModalCancel() {
|
||||
this.isManageModalVisible = false;
|
||||
}
|
||||
|
||||
onManageModalOk() {
|
||||
this.isManageModalOkLoading = true;
|
||||
if (this.pluginForm.valid) {
|
||||
const formData = new FormData();
|
||||
formData.append('name', this.pluginForm.get('name')?.value);
|
||||
formData.append('jarFile', this.fileList[0] as any);
|
||||
formData.append('enableStatus', this.pluginForm.get('enableStatus')?.value);
|
||||
this.pluginService.uploadPlugin(formData).subscribe((message: any) => {
|
||||
if (message.code === 0) {
|
||||
this.isManageModalVisible = false;
|
||||
this.resetForm();
|
||||
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.new-success'), '');
|
||||
this.loadPluginsTable();
|
||||
} else {
|
||||
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.new-fail'), message.msg);
|
||||
}
|
||||
this.isManageModalOkLoading = false;
|
||||
});
|
||||
} else {
|
||||
Object.values(this.pluginForm.controls).forEach(control => {
|
||||
if (control.invalid) {
|
||||
control.markAsDirty();
|
||||
control.updateValueAndValidity({ onlySelf: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resetForm(): void {
|
||||
this.pluginForm.reset({
|
||||
name: null,
|
||||
enableStatus: true
|
||||
});
|
||||
this.fileList = [];
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CollectorComponent } from './collector/collector.component';
|
||||
import { DefineComponent } from './define/define.component';
|
||||
import { SettingPluginsComponent } from './plugins/plugin.component';
|
||||
import { MessageServerComponent } from './settings/message-server/message-server.component';
|
||||
import { ObjectStoreComponent } from './settings/object-store/object-store.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
@@ -31,6 +32,7 @@ import { SettingTagsComponent } from './tags/tags.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'tags', component: SettingTagsComponent },
|
||||
{ path: 'plugins', component: SettingPluginsComponent },
|
||||
{ path: 'collector', component: CollectorComponent },
|
||||
{ path: 'status', component: StatusComponent },
|
||||
{ path: 'define', component: DefineComponent, data: { titleI18n: 'menu.advanced.define' } },
|
||||
|
||||
@@ -31,10 +31,12 @@ import { NzListModule } from 'ng-zorro-antd/list';
|
||||
import { NzRadioModule } from 'ng-zorro-antd/radio';
|
||||
import { NzSwitchModule } from 'ng-zorro-antd/switch';
|
||||
import { NzTagModule } from 'ng-zorro-antd/tag';
|
||||
import { NzUploadComponent } from 'ng-zorro-antd/upload';
|
||||
import { ColorPickerModule } from 'ngx-color-picker';
|
||||
|
||||
import { CollectorComponent } from './collector/collector.component';
|
||||
import { DefineComponent } from './define/define.component';
|
||||
import { SettingPluginsComponent } from './plugins/plugin.component';
|
||||
import { SettingRoutingModule } from './setting-routing.module';
|
||||
import { MessageServerComponent } from './settings/message-server/message-server.component';
|
||||
import { ObjectStoreComponent } from './settings/object-store/object-store.component';
|
||||
@@ -51,7 +53,8 @@ const COMPONENTS: Array<Type<void>> = [
|
||||
SystemConfigComponent,
|
||||
ObjectStoreComponent,
|
||||
CollectorComponent,
|
||||
StatusComponent
|
||||
StatusComponent,
|
||||
SettingPluginsComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -70,7 +73,8 @@ const COMPONENTS: Array<Type<void>> = [
|
||||
NzCodeEditorModule,
|
||||
ClipboardModule,
|
||||
NzBadgeModule,
|
||||
NzRadioModule
|
||||
NzRadioModule,
|
||||
NzUploadComponent
|
||||
],
|
||||
declarations: COMPONENTS
|
||||
})
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PluginService } from './plugin.service';
|
||||
|
||||
describe('PluginService', () => {
|
||||
let service: PluginService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(PluginService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Message } from '../pojo/Message';
|
||||
import { Page } from '../pojo/Page';
|
||||
import { Plugin } from '../pojo/Plugin';
|
||||
import { Tag } from '../pojo/Tag';
|
||||
|
||||
const plugin_uri = '/plugin';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PluginService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
public loadPlugins(
|
||||
search: string | undefined,
|
||||
type: number | undefined,
|
||||
pageIndex: number,
|
||||
pageSize: number
|
||||
): Observable<Message<Page<Plugin>>> {
|
||||
pageIndex = pageIndex ? pageIndex : 0;
|
||||
pageSize = pageSize ? pageSize : 8;
|
||||
// 注意HttpParams是不可变对象 需要保存set后返回的对象为最新对象
|
||||
let httpParams = new HttpParams();
|
||||
httpParams = httpParams.appendAll({
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize
|
||||
});
|
||||
if (search != undefined && search != '' && search.trim() != '') {
|
||||
httpParams = httpParams.append('search', search.trim());
|
||||
}
|
||||
const options = { params: httpParams };
|
||||
return this.http.get<Message<Page<Plugin>>>(plugin_uri, options);
|
||||
}
|
||||
|
||||
public uploadPlugin(body: FormData): Observable<Message<any>> {
|
||||
return this.http.post<Message<any>>(plugin_uri, body);
|
||||
}
|
||||
|
||||
public updatePluginStatus(body: Plugin): Observable<Message<any>> {
|
||||
body.enableStatus = !body.enableStatus;
|
||||
return this.http.put<Message<any>>(plugin_uri, body);
|
||||
}
|
||||
|
||||
public newTags(body: Tag[]): Observable<Message<any>> {
|
||||
return this.http.post<Message<any>>(plugin_uri, body);
|
||||
}
|
||||
|
||||
public newTag(body: Tag): Observable<Message<any>> {
|
||||
const tags = [];
|
||||
tags.push(body);
|
||||
return this.http.post<Message<any>>(plugin_uri, tags);
|
||||
}
|
||||
|
||||
public editTag(body: Tag): Observable<Message<any>> {
|
||||
return this.http.put<Message<any>>(plugin_uri, body);
|
||||
}
|
||||
|
||||
public deletePlugins(pluginIds: Set<number>): Observable<Message<any>> {
|
||||
let httpParams = new HttpParams();
|
||||
pluginIds.forEach(pluginId => {
|
||||
httpParams = httpParams.append('ids', pluginId);
|
||||
});
|
||||
const options = { params: httpParams };
|
||||
return this.http.delete<Message<any>>(plugin_uri, options);
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,12 @@
|
||||
"i18n": "menu.advanced.tags",
|
||||
"icon": "anticon-tags",
|
||||
"link": "/setting/tags"
|
||||
},
|
||||
{
|
||||
"text": "plugins",
|
||||
"i18n": "menu.advanced.plugins",
|
||||
"icon": "anticon-usb",
|
||||
"link": "/setting/plugins"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"collector": "Collector Cluster",
|
||||
"tags": "Tags Manage",
|
||||
"define": "Monitor Template",
|
||||
"status": "Status Page"
|
||||
"status": "Status Page",
|
||||
"plugins": "Plugins Manage"
|
||||
},
|
||||
"extras": {
|
||||
"": "More",
|
||||
@@ -491,6 +492,7 @@
|
||||
"common.week.5": "Friday",
|
||||
"common.week.6": "Saturday",
|
||||
"common.time.unit.second": "Seconds",
|
||||
"common.file.select": "Select File",
|
||||
"validation.email.invalid": "Invalid email!",
|
||||
"validation.phone.invalid": "Invalid phone number!",
|
||||
"validation.verification-code.invalid": "Invalid verification code, should be 6 digits!",
|
||||
@@ -530,6 +532,16 @@
|
||||
"tag.bind.tip": "You can use tags for classification management.eg: assign tags to resources in production environment and test environment.",
|
||||
"tag.help": "Tags are everywhere in HertzBeat. We can apply tags in resource grouping, tag matching under rules and others. [Tag Manage] is used for unified management of tags, including adding, deleting, editing, etc. <br>You can use tags to classify and manage monitoring resources, such as binding labels for production and testing environments separately.",
|
||||
"tag.help.link": "https://hertzbeat.apache.org/zh-cn/docs/",
|
||||
"plugin.help": "In HertzBeat, we can use the plugin mechanism to perform some other operations after the alarm except notification. Plugin management is used for unified management of plugins, including upload and enable/disable operations.<br>For example, you can use the plugin mechanism to execute specific scripts or SQL after the alarm occurs.",
|
||||
"plugin.help.link": "https://hertzbeat.apache.org/docs/help/plugin",
|
||||
"plugin.upload": "Upload Plugin",
|
||||
"plugin.name": "Plugin Name",
|
||||
"plugin.type": "Plugin Type",
|
||||
"plugin.status": "Enabled Status",
|
||||
"plugin.jar.file": "Jar File",
|
||||
"plugin.delete": "Delete Plugin",
|
||||
"plugin.type.POST_ALERT": "POST ALERT",
|
||||
"plugin.search": "Search plugins",
|
||||
"define.help": "The monitor templates define each monitoring type, parameter variable, metrics info, collection protocol, etc. You can select an existing monitoring template from the drop-down menu then make modifications according to your own needs. The bottom-left area is the compare area and the bottom-right area is the editing place. <br> You can also click \"New Monitor Type\" to custom define an new type. Currently supported protocols include<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-http'> HTTP</a>, <a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jdbc'>JDBC</a>, <a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-ssh'>SSH</a>, <a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jmx'>JMX</a>, <a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-snmp'> SNMP</a>. <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/template'>Monitor Templates</a>.",
|
||||
"define.help.link": "https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-point/",
|
||||
"define.save-apply": "Save And Apply",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"collector": "采集集群",
|
||||
"tags": "标签管理",
|
||||
"define": "监控模版",
|
||||
"status": "状态页面"
|
||||
"status": "状态页面",
|
||||
"plugins": "插件管理"
|
||||
},
|
||||
"extras": {
|
||||
"": "更多",
|
||||
@@ -492,6 +493,7 @@
|
||||
"common.week.5": "星期五",
|
||||
"common.week.6": "星期六",
|
||||
"common.time.unit.second": "秒",
|
||||
"common.file.select": "选择文件",
|
||||
"app.theme.default": "浅色主题",
|
||||
"app.theme.dark": "深色主题",
|
||||
"app.theme.compact": "紧凑主题",
|
||||
@@ -529,6 +531,16 @@
|
||||
"tag.bind.tip": "您可以使用标签进行监控资源的分类管理, 例如给资源分别绑定生产环境、测试环境的标签。",
|
||||
"tag.help": "标签在 HertzBeat 中无处不在,我们可以应用标签在资源分组,规则下的标签匹配等场景。标签管理用于对标签的统一管理维护,包含新增,删除,编辑等操作。<br>例如:您可以使用标签对监控资源进行分类管理,给资源分别绑定生产环境、测试环境的标签,在告警通知时通过标签匹配不同的通知人。",
|
||||
"tag.help.link": "https://hertzbeat.apache.org/zh-cn/docs/",
|
||||
"plugin.help": "在HertzBeat中,我们可以通过插件机制在告警后执行一些除通知以外的其他操作。插件管理用于对插件的统一管理,包括上传和,启用禁用等操作。<br>例如:您可以通过插件机制实现在告警出现后,执行特定的脚本或SQL等操作。",
|
||||
"plugin.help.link": "https://hertzbeat.apache.org/zh-cn/docs/help/plugin",
|
||||
"plugin.upload": "上传插件",
|
||||
"plugin.name": "插件名称",
|
||||
"plugin.status": "启用状态",
|
||||
"plugin.jar.file": "Jar包",
|
||||
"plugin.delete": "刪除插件",
|
||||
"plugin.type": "插件类型",
|
||||
"plugin.type.POST_ALERT": "告警后",
|
||||
"plugin.search": "搜索插件",
|
||||
"define.help": "监控模版定义每一个监控类型,类型的参数变量,指标信息,采集协议等。您可根据需求在下拉菜单中选择已有监控模板修改。左下区域为对照区,右下区域为编辑区。<br>您也可以点击“<i>新增监控类型</i>”来自定义新的的监控类型,目前支持 <a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-http'> HTTP 协议</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jdbc'>JDBC协议</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-ssh'>SSH协议</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jmx'> JMX 协议</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-snmp'> SNMP 协议</a>,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/template'>点击查看监控模板</a>。\n",
|
||||
"define.help.link": "https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-point/",
|
||||
"define.save-apply": "保存并应用",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"collector": "採集集群",
|
||||
"tags": "標簽管理",
|
||||
"define": "監控模版",
|
||||
"status": "狀態頁面"
|
||||
"status": "狀態頁面",
|
||||
"plugins": "外掛管理"
|
||||
},
|
||||
"extras": {
|
||||
"": "更多",
|
||||
@@ -491,6 +492,7 @@
|
||||
"common.button.setting": "配置",
|
||||
"common.button.delete": "刪除",
|
||||
"common.time.unit.second": "秒",
|
||||
"common.file.select": "選擇文件",
|
||||
"app.theme.default": "淺色主題",
|
||||
"app.theme.dark": "深色主題",
|
||||
"app.theme.compact": "緊湊主題",
|
||||
@@ -528,6 +530,16 @@
|
||||
"tag.bind.tip": "您可以使用標簽進行監控資源的分類管理, 例如給資源分別綁定生産環境、測試環境的標簽。",
|
||||
"tag.help": "標簽在 HertzBeat 中無處不在,我們可以應用標簽在資源分組,規則下的標簽匹配等場景。標簽管理用于對標簽的統壹管理維護,包含新增,刪除,編輯等操作。<br>例如:您可以使用標簽對監控資源進行分類管理,給資源分別綁定生産環境、測試環境的標簽,在告警通知時通過標簽匹配不同的通知人。",
|
||||
"tag.help.link": "https://hertzbeat.apache.org/zh-cn/docs/",
|
||||
"plugin.help": "在HertzBeat中,我們可以透過外掛機制在警告後執行一些除通知以外的其他操作。外掛程式管理用於對外掛程式的統一管理,包括上傳和,啟用停用等操作。<br>例如:您可以透過外掛機制實現在警告出現後,執行特定的腳本或SQL等操作。",
|
||||
"plugin.help.link": "https://hertzbeat.apache.org/zh-cn/docs/help/plugin",
|
||||
"plugin.upload": "上傳外掛",
|
||||
"plugin.name": "插件名稱",
|
||||
"plugin.type": "插件類型",
|
||||
"plugin.status": "啟用狀態",
|
||||
"plugin.jar.file": "Jar包",
|
||||
"plugin.delete": "刪除插件",
|
||||
"plugin.type.POST_ALERT": "告警後",
|
||||
"plugin.search": "搜尋插件",
|
||||
"define.help": "監控模版定義每一個監控類型,類型的參數變量,指標信息,採集協議等。您可根據需求在下拉功能表中選擇已有監控模版進行修改。右下區域為編輯區,左下區域為對照區。<br>您也可以點擊“<i>新增監控類型</i>”來自定義新的的監控類型,現支持<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-http'> HTTP協議</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jdbc'>JDBC協定</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-ssh'>SSH協定</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jmx'> JMX協定</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-snmp'> SNMP協定</a>,<a href='https://hertzbeat.apache.org/zh-cn/docs/template'>點擊查看監控範本</a>。",
|
||||
"define.help.link": "https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-point/",
|
||||
"define.save-apply": "保存並應用",
|
||||
|
||||
Reference in New Issue
Block a user