From bbc4db4ddfe4728d67841c440699ebc00145f677 Mon Sep 17 00:00:00 2001 From: Mactavish Cui Date: Fri, 10 Jan 2025 10:24:51 +0800 Subject: [PATCH] [Feature][admin] Task submit approval (#4070) --- .../org/dinky/aop/TaskApprovalAspect.java | 80 ++++ .../aop/TaskOperationPermissionAspect.java | 44 +- .../java/org/dinky/configure/AppConfig.java | 3 +- .../dinky/configure/MybatisPlusConfig.java | 3 +- .../dinky/controller/ApprovalController.java | 120 +++++ .../org/dinky/controller/TaskController.java | 3 + .../java/org/dinky/data/dto/ApprovalDTO.java | 50 +++ .../java/org/dinky/data/model/Approval.java | 96 ++++ .../java/org/dinky/mapper/ApprovalMapper.java | 32 ++ .../org/dinky/service/ApprovalService.java | 75 ++++ .../service/impl/ApprovalServiceImpl.java | 293 ++++++++++++ .../main/java/org/dinky/utils/AspectUtil.java | 67 +++ .../migration/h2/V20241219.1.2.1__release.sql | 41 ++ .../mysql/V20241219.1.2.1__release.sql | 74 ++++ .../postgresql/V20241219.1.2.1__release.sql | 75 ++++ .../main/resources/mapper/ApprovalMapper.xml | 94 ++++ .../data/annotations/CheckTaskApproval.java | 37 ++ .../org/dinky/data/enums/ApprovalEvent.java | 38 ++ .../org/dinky/data/enums/ApprovalStatus.java | 57 +++ .../java/org/dinky/data/enums/Status.java | 13 +- .../dinky/data/model/SystemConfiguration.java | 28 ++ .../resources/i18n/messages_en_US.properties | 10 + .../resources/i18n/messages_zh_CN.properties | 9 + dinky-web/config/routes.ts | 6 + .../src/components/Icons/CustomIcons.tsx | 27 ++ dinky-web/src/locales/en-US/menu.ts | 7 +- dinky-web/src/locales/en-US/pages.ts | 33 +- dinky-web/src/locales/zh-CN/menu.ts | 7 +- dinky-web/src/locales/zh-CN/pages.ts | 34 +- .../components/ApprovalModal/index.tsx | 81 ++++ .../components/ApprovalTable/index.tsx | 419 ++++++++++++++++++ .../components/TaskInfoModal/index.tsx | 131 ++++++ .../src/pages/AuthCenter/Approval/index.tsx | 107 +++++ .../CenterTabContent/SqlTask/index.tsx | 64 +++ .../SettingOverView/ApprovalConfig/index.tsx | 55 +++ .../SettingOverView/constants.ts | 3 +- .../GlobalSetting/SettingOverView/index.tsx | 27 +- dinky-web/src/services/endpoints.tsx | 13 +- dinky-web/src/types/AuthCenter/data.d.ts | 33 ++ dinky-web/src/types/AuthCenter/init.d.ts | 10 +- dinky-web/src/types/AuthCenter/state.d.ts | 6 +- dinky-web/src/types/Public/constants.tsx | 2 + 42 files changed, 2351 insertions(+), 56 deletions(-) create mode 100644 dinky-admin/src/main/java/org/dinky/aop/TaskApprovalAspect.java create mode 100644 dinky-admin/src/main/java/org/dinky/controller/ApprovalController.java create mode 100644 dinky-admin/src/main/java/org/dinky/data/dto/ApprovalDTO.java create mode 100644 dinky-admin/src/main/java/org/dinky/data/model/Approval.java create mode 100644 dinky-admin/src/main/java/org/dinky/mapper/ApprovalMapper.java create mode 100644 dinky-admin/src/main/java/org/dinky/service/ApprovalService.java create mode 100644 dinky-admin/src/main/java/org/dinky/service/impl/ApprovalServiceImpl.java create mode 100644 dinky-admin/src/main/java/org/dinky/utils/AspectUtil.java create mode 100644 dinky-admin/src/main/resources/db/migration/h2/V20241219.1.2.1__release.sql create mode 100644 dinky-admin/src/main/resources/db/migration/mysql/V20241219.1.2.1__release.sql create mode 100644 dinky-admin/src/main/resources/db/migration/postgresql/V20241219.1.2.1__release.sql create mode 100644 dinky-admin/src/main/resources/mapper/ApprovalMapper.xml create mode 100644 dinky-common/src/main/java/org/dinky/data/annotations/CheckTaskApproval.java create mode 100644 dinky-common/src/main/java/org/dinky/data/enums/ApprovalEvent.java create mode 100644 dinky-common/src/main/java/org/dinky/data/enums/ApprovalStatus.java create mode 100644 dinky-web/src/pages/AuthCenter/Approval/components/ApprovalModal/index.tsx create mode 100644 dinky-web/src/pages/AuthCenter/Approval/components/ApprovalTable/index.tsx create mode 100644 dinky-web/src/pages/AuthCenter/Approval/components/TaskInfoModal/index.tsx create mode 100644 dinky-web/src/pages/AuthCenter/Approval/index.tsx create mode 100644 dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/ApprovalConfig/index.tsx diff --git a/dinky-admin/src/main/java/org/dinky/aop/TaskApprovalAspect.java b/dinky-admin/src/main/java/org/dinky/aop/TaskApprovalAspect.java new file mode 100644 index 0000000000..597e1cf4dd --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/aop/TaskApprovalAspect.java @@ -0,0 +1,80 @@ +/* + * + * 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.dinky.aop; + +import org.dinky.data.annotations.CheckTaskApproval; +import org.dinky.data.enums.Status; +import org.dinky.data.exception.BusException; +import org.dinky.data.model.SystemConfiguration; +import org.dinky.utils.AspectUtil; + +import java.lang.reflect.Method; +import java.util.Objects; + +import javax.annotation.Resource; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +@Aspect +@Slf4j +@Component +public class TaskApprovalAspect { + @Resource + private ApplicationContext applicationContext; + + /** + * Check whether the user has the permission to perform the task. + * + * @param joinPoint task operation + * @param checkTaskApproval check task approval aspect + * @return join point execute result + * @throws Throwable exception if task still need approval + */ + @Around(value = "@annotation(checkTaskApproval)") + public Object processAround(ProceedingJoinPoint joinPoint, CheckTaskApproval checkTaskApproval) throws Throwable { + if (SystemConfiguration.getInstances().enableTaskSubmitApprove()) { + Class checkParam = checkTaskApproval.checkParam(); + Object param = AspectUtil.getParam(joinPoint, checkParam); + if (Objects.nonNull(param)) { + Object bean = applicationContext.getBean(checkTaskApproval.checkInterface()); + Class clazz = bean.getClass(); + Method method = clazz.getMethod(checkTaskApproval.checkMethod(), param.getClass()); + Object invoke = method.invoke(bean, param); + if (invoke != null && (Boolean) invoke) { + throw new BusException(Status.SYS_APPROVAL_TASK_NOT_APPROVED); + } + } + } + + Object result; + try { + result = joinPoint.proceed(); + } catch (Throwable e) { + throw e; + } + return result; + } +} diff --git a/dinky-admin/src/main/java/org/dinky/aop/TaskOperationPermissionAspect.java b/dinky-admin/src/main/java/org/dinky/aop/TaskOperationPermissionAspect.java index 271d6d7f64..c769dee1ca 100644 --- a/dinky-admin/src/main/java/org/dinky/aop/TaskOperationPermissionAspect.java +++ b/dinky-admin/src/main/java/org/dinky/aop/TaskOperationPermissionAspect.java @@ -25,9 +25,8 @@ import org.dinky.data.enums.TaskOwnerLockStrategyEnum; import org.dinky.data.exception.BusException; import org.dinky.data.model.SystemConfiguration; +import org.dinky.utils.AspectUtil; -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Objects; @@ -36,7 +35,6 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; @@ -65,7 +63,7 @@ public Object processAround(ProceedingJoinPoint joinPoint, CheckTaskOwner checkT SystemConfiguration.getInstances().GetTaskOwnerLockStrategyValue()) && BaseConstant.ADMIN_ID != StpUtil.getLoginIdAsInt()) { Class checkParam = checkTaskOwner.checkParam(); - Object param = getParam(joinPoint, checkParam); + Object param = AspectUtil.getParam(joinPoint, checkParam); if (Objects.nonNull(param)) { Object bean = applicationContext.getBean(checkTaskOwner.checkInterface()); Class clazz = bean.getClass(); @@ -85,42 +83,4 @@ public Object processAround(ProceedingJoinPoint joinPoint, CheckTaskOwner checkT } return result; } - - private Object getParam(ProceedingJoinPoint joinPoint, Class paramAnno) throws IllegalAccessException { - Object[] params = joinPoint.getArgs(); - if (params.length == 0) { - return null; - } - - Object paramObj = null; - // Get the method, here you can convert the signature strong to MethodSignature - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - Method method = signature.getMethod(); - - Annotation[][] annotations = method.getParameterAnnotations(); - for (int i = 0; i < annotations.length; i++) { - Object param = params[i]; - if (param == null) { - continue; - } - Annotation[] paramAnn = annotations[i]; - for (Annotation annotation : paramAnn) { - if (annotation.annotationType() == paramAnno) { - paramObj = param; - break; - } - } - if (paramObj == null) { - Field[] fields = param.getClass().getDeclaredFields(); - for (Field field : fields) { - if (field.isAnnotationPresent(paramAnno)) { - field.setAccessible(true); - paramObj = field.get(param); - break; - } - } - } - } - return paramObj; - } } diff --git a/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java b/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java index c5d155ceac..e444a0d2eb 100644 --- a/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java +++ b/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java @@ -113,6 +113,7 @@ public void addInterceptors(InterceptorRegistry registry) { .addPathPatterns("/api/role/**") .addPathPatterns("/api/fragment/**") .addPathPatterns("/api/git/**") - .addPathPatterns("/api/jar/*"); + .addPathPatterns("/api/jar/*") + .addPathPatterns("/api/approval/*"); } } diff --git a/dinky-admin/src/main/java/org/dinky/configure/MybatisPlusConfig.java b/dinky-admin/src/main/java/org/dinky/configure/MybatisPlusConfig.java index 685adc9c87..e349706822 100644 --- a/dinky-admin/src/main/java/org/dinky/configure/MybatisPlusConfig.java +++ b/dinky-admin/src/main/java/org/dinky/configure/MybatisPlusConfig.java @@ -77,7 +77,8 @@ public class MybatisPlusConfig { "dinky_task", "dinky_task_statement", "dinky_git_project", - "dinky_task_version"); + "dinky_task_version", + "dinky_approval"); @Bean @Profile("postgresql") diff --git a/dinky-admin/src/main/java/org/dinky/controller/ApprovalController.java b/dinky-admin/src/main/java/org/dinky/controller/ApprovalController.java new file mode 100644 index 0000000000..16dfc1797d --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/controller/ApprovalController.java @@ -0,0 +1,120 @@ +/* + * + * 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.dinky.controller; + +import org.dinky.data.annotations.CheckTaskOwner; +import org.dinky.data.annotations.ProcessId; +import org.dinky.data.annotations.TaskId; +import org.dinky.data.dto.ApprovalDTO; +import org.dinky.data.enums.ApprovalEvent; +import org.dinky.data.model.Approval; +import org.dinky.data.model.rbac.User; +import org.dinky.data.result.ProTableResult; +import org.dinky.data.result.Result; +import org.dinky.service.ApprovalService; +import org.dinky.service.TaskService; + +import java.util.List; + +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; + +import com.fasterxml.jackson.databind.JsonNode; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@Slf4j +@Api(tags = "Approval Controller", hidden = true) +@RequestMapping("/api/approval") +@SaCheckLogin +@RequiredArgsConstructor +public class ApprovalController { + + private final ApprovalService approvalService; + + @PostMapping("/getSubmittedApproval") + @ApiOperation("Get all approvals submitted by current user") + ProTableResult getSubmittedApproval(@RequestBody JsonNode para) { + return approvalService.getSubmittedApproval(para); + } + + @PostMapping("/getApprovalToBeReviewed") + @ApiOperation("Get all approvals current user is required for review") + ProTableResult getApprovalToBeReviewed(@RequestBody JsonNode para) { + return approvalService.getApprovalToBeReviewed(para); + } + + @CheckTaskOwner(checkParam = TaskId.class, checkInterface = TaskService.class) + @PutMapping("/createTaskApproval") + Result createTaskApproval(@TaskId @ProcessId @RequestParam Integer taskId) { + return Result.succeed(approvalService.createTaskApproval(taskId)); + } + + @PostMapping("/submit") + @ApiOperation("Submit approval") + Result submit(@RequestBody ApprovalDTO approvalDTO) { + approvalService.handleApproveEvent(ApprovalEvent.SUBMIT, approvalDTO); + return Result.succeed(); + } + + @PostMapping("/withdraw") + @ApiOperation("Withdraw approval") + Result withdraw(@RequestBody ApprovalDTO approvalDTO) { + approvalService.handleApproveEvent(ApprovalEvent.WITHDRAW, approvalDTO); + return Result.succeed(); + } + + @PostMapping("/approve") + @ApiOperation("Approve approval") + Result approve(@RequestBody ApprovalDTO approvalDTO) { + approvalService.handleApproveEvent(ApprovalEvent.APPROVE, approvalDTO); + return Result.succeed(); + } + + @PostMapping("/reject") + @ApiOperation("Reject approval") + Result reject(@RequestBody ApprovalDTO approvalDTO) { + approvalService.handleApproveEvent(ApprovalEvent.REJECT, approvalDTO); + return Result.succeed(); + } + + @PostMapping("/cancel") + @ApiOperation("Reject approval") + Result cancel(@RequestBody ApprovalDTO approvalDTO) { + approvalService.handleApproveEvent(ApprovalEvent.CANCEL, approvalDTO); + return Result.succeed(); + } + + @GetMapping("/getReviewers") + @ApiOperation("Get reviewers that from current tenant") + Result> getReviewers(@RequestParam Integer tenantId) { + return Result.succeed(approvalService.getTaskReviewerList(tenantId)); + } +} diff --git a/dinky-admin/src/main/java/org/dinky/controller/TaskController.java b/dinky-admin/src/main/java/org/dinky/controller/TaskController.java index f9edef0b57..40e792d48b 100644 --- a/dinky-admin/src/main/java/org/dinky/controller/TaskController.java +++ b/dinky-admin/src/main/java/org/dinky/controller/TaskController.java @@ -19,6 +19,7 @@ package org.dinky.controller; +import org.dinky.data.annotations.CheckTaskApproval; import org.dinky.data.annotations.CheckTaskOwner; import org.dinky.data.annotations.ExecuteProcess; import org.dinky.data.annotations.Log; @@ -44,6 +45,7 @@ import org.dinky.gateway.result.SavePointResult; import org.dinky.job.JobResult; import org.dinky.mybatis.annotation.Save; +import org.dinky.service.ApprovalService; import org.dinky.service.TaskService; import org.dinky.trans.ExecuteJarParseStrategyUtil; import org.dinky.utils.SqlUtil; @@ -97,6 +99,7 @@ public class TaskController { @ApiOperation("Submit Task") @Log(title = "Submit Task", businessType = BusinessType.SUBMIT) @ExecuteProcess(type = ProcessType.FLINK_SUBMIT) + @CheckTaskApproval(checkParam = TaskId.class, checkInterface = ApprovalService.class) @CheckTaskOwner(checkParam = TaskId.class, checkInterface = TaskService.class) public Result submitTask(@TaskId @ProcessId @RequestParam Integer id) throws Exception { JobResult jobResult = diff --git a/dinky-admin/src/main/java/org/dinky/data/dto/ApprovalDTO.java b/dinky-admin/src/main/java/org/dinky/data/dto/ApprovalDTO.java new file mode 100644 index 0000000000..ceb1fc160d --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/data/dto/ApprovalDTO.java @@ -0,0 +1,50 @@ +/* + * + * 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.dinky.data.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@ApiModel(value = "ApprovalDTO", description = "Approval Data Transfer Object") +public class ApprovalDTO { + @ApiModelProperty(value = "Approval ID", dataType = "Integer", example = "123", notes = "The ID of the approval") + private Integer id; + + @ApiModelProperty(value = "Task ID", dataType = "Integer", example = "123", notes = "The ID of the task") + private Integer taskId; + + @ApiModelProperty( + value = "Reviewer id required for current approval", + dataType = "Integer", + example = "123", + notes = "The ID of the reviewer") + private Integer reviewer; + + @ApiModelProperty( + value = "Comment", + dataType = "String", + example = "Looks good to me/Please take a look", + notes = "Comment from reviewer or submitter") + private String comment; +} diff --git a/dinky-admin/src/main/java/org/dinky/data/model/Approval.java b/dinky-admin/src/main/java/org/dinky/data/model/Approval.java new file mode 100644 index 0000000000..6cf446ad4d --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/data/model/Approval.java @@ -0,0 +1,96 @@ +/* + * + * 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.dinky.data.model; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@Builder +@EqualsAndHashCode(callSuper = false) +@TableName("dinky_approval") +@ApiModel(value = "Approval", description = "Approval instance") +public class Approval extends Model { + + @TableId(type = IdType.AUTO) + @ApiModelProperty(value = "ID", dataType = "Integer", notes = "Unique identifier for the approval") + private Integer id; + + @ApiModelProperty(value = "Task Id", dataType = "Integer", notes = "Task identifier for the task approval linked") + private Integer taskId; + + @ApiModelProperty(value = "Tenant Id", dataType = "Integer", notes = "Tenant id of current approval") + private Integer tenantId; + + @ApiModelProperty( + value = "Previous Task Version", + dataType = "Integer", + notes = "Previous online version before this task is submitted") + private Integer previousTaskVersion; + + @ApiModelProperty(value = "Current Task Version", dataType = "Integer", notes = "Task version required for publish") + private Integer currentTaskVersion; + + @ApiModelProperty(value = "Approval status", dataType = "String", notes = "Approval status") + private String status; + + @ApiModelProperty(value = "Submitter", dataType = "Integer", notes = "Submitter user id") + private Integer submitter; + + @ApiModelProperty(value = "Submitter Comment", dataType = "String", notes = "Submitter comment") + private String submitterComment; + + @ApiModelProperty(value = "Reviewer", dataType = "Integer", notes = "Reviewer user id") + private Integer reviewer; + + @ApiModelProperty(value = "Reviewer Comment", dataType = "String", notes = "Reviewer comment") + private String reviewerComment; + + @TableField(fill = FieldFill.INSERT) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "Create Time", dataType = "Date", notes = "Timestamp when the approval was created") + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "Update Time", dataType = "Date", notes = "Timestamp when the approval was updated") + private LocalDateTime updateTime; +} diff --git a/dinky-admin/src/main/java/org/dinky/mapper/ApprovalMapper.java b/dinky-admin/src/main/java/org/dinky/mapper/ApprovalMapper.java new file mode 100644 index 0000000000..9ef3d6d326 --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/mapper/ApprovalMapper.java @@ -0,0 +1,32 @@ +/* + * + * 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.dinky.mapper; + +import org.dinky.data.model.Approval; +import org.dinky.mybatis.mapper.SuperMapper; + +import java.util.List; + +import org.mapstruct.Mapper; + +@Mapper +public interface ApprovalMapper extends SuperMapper { + List getApprovalByTaskId(Integer taskId); +} diff --git a/dinky-admin/src/main/java/org/dinky/service/ApprovalService.java b/dinky-admin/src/main/java/org/dinky/service/ApprovalService.java new file mode 100644 index 0000000000..7d9907f93b --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/service/ApprovalService.java @@ -0,0 +1,75 @@ +/* + * + * 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.dinky.service; + +import org.dinky.data.dto.ApprovalDTO; +import org.dinky.data.enums.ApprovalEvent; +import org.dinky.data.model.Approval; +import org.dinky.data.model.rbac.User; +import org.dinky.data.result.ProTableResult; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.fasterxml.jackson.databind.JsonNode; + +public interface ApprovalService extends IService { + + /** + * get all approvals submitted by current user + * @return approval list submitted by current user + */ + ProTableResult getSubmittedApproval(JsonNode params); + + /** + * get all approvals current user need to review + * @return Approval list current user need to review + */ + ProTableResult getApprovalToBeReviewed(JsonNode params); + + /** + * create a new approval for task + * @param taskId task id linked to approval + * @return if an approval for this task has an approval already in created status, return it, otherwise return a new one + */ + Approval createTaskApproval(Integer taskId); + + /** + * handle approval + * @param event operation event + * @param approvalDTO approval DTO + */ + void handleApproveEvent(ApprovalEvent event, ApprovalDTO approvalDTO); + + /** + * check if a task need approve + * @param taskId task id + * @return true if current task need approve + */ + boolean needApprove(Integer taskId); + + /** + * get the reviewers of current tenant + * + * @param tenantId tenant Id + * @return A list of {@link User} objects representing the users that can review current task + */ + List getTaskReviewerList(Integer tenantId); +} diff --git a/dinky-admin/src/main/java/org/dinky/service/impl/ApprovalServiceImpl.java b/dinky-admin/src/main/java/org/dinky/service/impl/ApprovalServiceImpl.java new file mode 100644 index 0000000000..837af3aab6 --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/service/impl/ApprovalServiceImpl.java @@ -0,0 +1,293 @@ +/* + * + * 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.dinky.service.impl; + +import org.dinky.assertion.Asserts; +import org.dinky.data.constant.BaseConstant; +import org.dinky.data.dto.ApprovalDTO; +import org.dinky.data.dto.TaskDTO; +import org.dinky.data.enums.ApprovalEvent; +import org.dinky.data.enums.ApprovalStatus; +import org.dinky.data.enums.JobLifeCycle; +import org.dinky.data.enums.Status; +import org.dinky.data.exception.BusException; +import org.dinky.data.exception.DinkyException; +import org.dinky.data.model.Approval; +import org.dinky.data.model.SystemConfiguration; +import org.dinky.data.model.rbac.Role; +import org.dinky.data.model.rbac.User; +import org.dinky.data.result.ProTableResult; +import org.dinky.mapper.ApprovalMapper; +import org.dinky.mybatis.service.impl.SuperServiceImpl; +import org.dinky.service.ApprovalService; +import org.dinky.service.RoleService; +import org.dinky.service.TaskService; +import org.dinky.service.UserService; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.JsonNode; + +import cn.dev33.satoken.stp.StpUtil; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ApprovalServiceImpl extends SuperServiceImpl implements ApprovalService { + + private final TaskService taskService; + private final UserService userService; + private final RoleService roleService; + private Map> validPreStatusMap; + private Map operationResultMap; + + @Override + public ProTableResult getSubmittedApproval(JsonNode params) { + Map paraMap = new HashMap<>(); + paraMap.put("submitter", StpUtil.getLoginIdAsInt()); + return super.selectForProTable(params, paraMap); + } + + @Override + public ProTableResult getApprovalToBeReviewed(JsonNode params) { + Map paraMap = new HashMap<>(); + paraMap.put("reviewer", StpUtil.getLoginIdAsInt()); + return super.selectForProTable(params, paraMap); + } + + @Override + public Approval createTaskApproval(Integer taskId) { + // return existed created approval + List previousApproval = baseMapper.getApprovalByTaskId(taskId); + if (Asserts.isNotNullCollection(previousApproval)) { + for (Approval approval : previousApproval) { + if (ApprovalStatus.CREATED.equalVal(approval.getStatus())) { + return approval; + } + } + } + + // build previous version + Integer previousTaskVersion = null; + LocalDateTime latestApprovalTime = null; + for (Approval approval : previousApproval) { + if (ApprovalStatus.APPROVED.equalVal(approval.getStatus())) { + if (latestApprovalTime == null || latestApprovalTime.isBefore(approval.getUpdateTime())) { + latestApprovalTime = approval.getUpdateTime(); + previousTaskVersion = approval.getCurrentTaskVersion(); + } + } + } + + TaskDTO taskDTO = taskService.getTaskInfoById(taskId); + Approval createdApproval = Approval.builder() + .taskId(taskId) + .submitter(StpUtil.getLoginIdAsInt()) + .status(ApprovalStatus.CREATED.getValue()) + .previousTaskVersion(previousTaskVersion) + .currentTaskVersion(taskDTO.getVersionId()) + .build(); + baseMapper.insert(createdApproval); + return createdApproval; + } + + @Override + public boolean needApprove(Integer taskId) { + if (!SystemConfiguration.getInstances().enableTaskSubmitApprove()) { + return false; + } + // only published task can be approved + TaskDTO byId = taskService.getTaskInfoById(taskId); + if (!JobLifeCycle.PUBLISH.equalsValue(byId.getStep())) { + return true; + } + // check approval version + List approvalList = baseMapper.getApprovalByTaskId(taskId); + for (Approval approval : approvalList) { + if (ApprovalStatus.APPROVED.equals(ApprovalStatus.fromValue(approval.getStatus())) + && approval.getCurrentTaskVersion().equals(byId.getVersionId())) { + return false; + } + } + return true; + } + + @Override + public List getTaskReviewerList(Integer tenantId) { + // get users with reviewer role + Set reviewerRoles = SystemConfiguration.getInstances().getReviewerRoles(); + List roles = roleService.list(new LambdaQueryWrapper() + .in(Role::getRoleCode, reviewerRoles) + .eq(Role::getTenantId, tenantId)); + // get super admin + User superAdmin = userService.getById(BaseConstant.ADMIN_ID); + Map userMap = new HashMap<>(); + userMap.put(BaseConstant.ADMIN_ID, superAdmin); + for (Role role : roles) { + List userList = roleService.getUserListByRoleId(role.getId()); + for (User user : userList) { + if (SystemConfiguration.getInstances().enforceCrossView() + && user.getId().equals(StpUtil.getLoginIdAsInt())) { + continue; + } + userMap.put(user.getId(), user); + } + } + return new ArrayList<>(userMap.values()); + } + + public void handleApproveEvent(ApprovalEvent event, ApprovalDTO approvalDTO) { + Approval approval = baseMapper.selectById(approvalDTO.getId()); + // permission check + if (!checkApprovalPermission(approval, event)) { + throw new DinkyException("No operation permission!"); + } + // reviewer check + if (event.equals(ApprovalEvent.SUBMIT) && !isValidReviewer(approvalDTO.getReviewer())) { + throw new DinkyException("Reviewer is not valid!"); + } + // only one approval in process check + if (event.equals(ApprovalEvent.SUBMIT) && alreadyHaveOneInProcess(approval.getTaskId())) { + throw new BusException(Status.SYS_APPROVAL_DUPLICATE_APPROVAL_IN_PROCESS); + } + // status machine execute + if (!validPreStatusMap.get(event).contains(ApprovalStatus.fromValue(approval.getStatus()))) { + throw new DinkyException("Not a valid operation!"); + } + approval.setStatus(operationResultMap.get(event).getValue()); + // handle other information + updateApprovalFromDTOByEvent(approval, approvalDTO, event); + baseMapper.updateById(approval); + } + + /** + * check approval operation permission + * + * @param approval approval + * @param event operation event + * @return true if has permission + */ + private boolean checkApprovalPermission(Approval approval, ApprovalEvent event) { + // permission check + switch (event) { + case SUBMIT: + taskService.checkTaskOperatePermission(approval.getTaskId()); + case WITHDRAW: + case CANCEL: + return StpUtil.getLoginIdAsInt() == approval.getSubmitter(); + case APPROVE: + case REJECT: + return StpUtil.getLoginIdAsInt() == approval.getReviewer(); + default: + throw new DinkyException("No approval permission"); + } + } + + /** + * check if user has reviewer role + * @param reviewer user id + * @return true if reviewer is valid + */ + private boolean isValidReviewer(Integer reviewer) { + // super admin + if (reviewer == BaseConstant.ADMIN_ID) { + return true; + } + List roleList = roleService.getRoleByUserId(reviewer); + Set reviewerRoles = SystemConfiguration.getInstances().getReviewerRoles(); + for (Role role : roleList) { + // reviewer role or super admin + if (reviewerRoles.contains(role.getRoleName())) { + return true; + } + } + return false; + } + + /** + * check if already have one approval in process + * + * @param taskId task id + * @return true if an approval is in process + */ + private boolean alreadyHaveOneInProcess(Integer taskId) { + for (Approval approval : baseMapper.getApprovalByTaskId(taskId)) { + if (ApprovalStatus.isInProcess(ApprovalStatus.valueOf(approval.getStatus()))) { + return true; + } + } + return false; + } + + /** + * init a simple status machine to control approval status + */ + @PostConstruct + private void initSimpleStatusMachine() { + // valid pre status + validPreStatusMap = new HashMap<>(); + validPreStatusMap.put(ApprovalEvent.SUBMIT, new HashSet<>(Collections.singletonList(ApprovalStatus.CREATED))); + validPreStatusMap.put(ApprovalEvent.CANCEL, new HashSet<>(Collections.singletonList(ApprovalStatus.CREATED))); + validPreStatusMap.put( + ApprovalEvent.WITHDRAW, new HashSet<>(Collections.singletonList(ApprovalStatus.SUBMITTED))); + validPreStatusMap.put( + ApprovalEvent.APPROVE, new HashSet<>(Collections.singletonList(ApprovalStatus.SUBMITTED))); + validPreStatusMap.put(ApprovalEvent.REJECT, new HashSet<>(Collections.singletonList(ApprovalStatus.SUBMITTED))); + // operation result + operationResultMap = new HashMap<>(); + operationResultMap.put(ApprovalEvent.SUBMIT, ApprovalStatus.SUBMITTED); + operationResultMap.put(ApprovalEvent.CANCEL, ApprovalStatus.CANCELED); + operationResultMap.put(ApprovalEvent.WITHDRAW, ApprovalStatus.CREATED); + operationResultMap.put(ApprovalEvent.APPROVE, ApprovalStatus.APPROVED); + operationResultMap.put(ApprovalEvent.REJECT, ApprovalStatus.REJECTED); + } + + /** + * update necessary info of approval from DTO + * + * @param target target approval + * @param source source DTO + * @param event approval event, different event need different info + */ + private void updateApprovalFromDTOByEvent(Approval target, ApprovalDTO source, ApprovalEvent event) { + switch (event) { + case SUBMIT: + target.setReviewer(source.getReviewer()); + target.setSubmitterComment(source.getComment()); + break; + case APPROVE: + case REJECT: + target.setReviewerComment(source.getComment()); + break; + } + } +} diff --git a/dinky-admin/src/main/java/org/dinky/utils/AspectUtil.java b/dinky-admin/src/main/java/org/dinky/utils/AspectUtil.java new file mode 100644 index 0000000000..21b5a616ad --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/utils/AspectUtil.java @@ -0,0 +1,67 @@ +/* + * + * 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.dinky.utils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; + +public class AspectUtil { + public static Object getParam(ProceedingJoinPoint joinPoint, Class paramAnno) throws IllegalAccessException { + Object[] params = joinPoint.getArgs(); + if (params.length == 0) { + return null; + } + + Object paramObj = null; + // Get the method, here you can convert the signature strong to MethodSignature + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + Annotation[][] annotations = method.getParameterAnnotations(); + for (int i = 0; i < annotations.length; i++) { + Object param = params[i]; + if (param == null) { + continue; + } + Annotation[] paramAnn = annotations[i]; + for (Annotation annotation : paramAnn) { + if (annotation.annotationType() == paramAnno) { + paramObj = param; + break; + } + } + if (paramObj == null) { + Field[] fields = param.getClass().getDeclaredFields(); + for (Field field : fields) { + if (field.isAnnotationPresent(paramAnno)) { + field.setAccessible(true); + paramObj = field.get(param); + break; + } + } + } + } + return paramObj; + } +} diff --git a/dinky-admin/src/main/resources/db/migration/h2/V20241219.1.2.1__release.sql b/dinky-admin/src/main/resources/db/migration/h2/V20241219.1.2.1__release.sql new file mode 100644 index 0000000000..ccc182fdd6 --- /dev/null +++ b/dinky-admin/src/main/resources/db/migration/h2/V20241219.1.2.1__release.sql @@ -0,0 +1,41 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +--- ---------------------------- +-- Table structure for dinky_approval +-- ---------------------------- +CREATE TABLE IF NOT EXISTS dinky_approval +( + id int(11) AUTO_INCREMENT COMMENT 'id', + task_id int(11) NOT NULL COMMENT 'task id', + tenant_id int(11) NOT NULL COMMENT 'tenant id' default 1, + previous_task_version int(11) DEFAULT NULL COMMENT 'previous version of task', + current_task_version int(11) NOT NULL COMMENT 'current version to be reviewed of task', + status VARCHAR(50) NOT NULL COMMENT 'approval status', + submitter int(11) NOT NULL COMMENT 'submitter user id', + submitter_comment varchar(255) DEFAULT NULL COMMENT 'submitter comment', + reviewer int(11) DEFAULT NULL COMMENT 'reviewer user id', + reviewer_comment varchar(255) DEFAULT NULL COMMENT 'reviewer comment', + create_time datetime(0) null DEFAULT null COMMENT 'create time', + update_time datetime(0) null DEFAULT null COMMENT 'update time', +) ENGINE = InnoDB ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- approval menu +-- ---------------------------- +INSERT INTO `dinky_sys_menu` (id, parent_id, name, path, component, perms, icon, type, display, order_num, create_time, + update_time, note) +VALUES (176, 4, '审批发布', '/auth/approval', './AuthCenter/Approval', 'auth:approval:operate', 'AuditOutlined', 'C', 0, 169, + '2024-12-10 12:13:00', '2024-12-10 12:13:00', NULL); + +insert into `dinky_sys_menu` (`id`, `parent_id`, `name`, `path`, `component`, `perms`, `icon`, `type`, `display`, + `order_num`, `create_time`, `update_time`, `note`) +values (177, 24, '审批配置', '/settings/globalsetting/approval', null, 'settings:globalsetting:approval', + 'SettingOutlined', 'F', 0, 170, '2024-12-30 23:45:30', '2024-12-30 23:45:30', null); + +insert into `dinky_sys_menu` (`id`, `parent_id`, `name`, `path`, `component`, `perms`, `icon`, `type`, `display`, + `order_num`, `create_time`, `update_time`, `note`) +values (178, 177, '编辑', '/settings/globalsetting/approval/edit', null, 'settings:globalsetting:approval:edit', + 'EditOutlined', 'F', 0, 171, '2024-12-30 23:45:30', '2024-12-30 23:45:30', null); + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/dinky-admin/src/main/resources/db/migration/mysql/V20241219.1.2.1__release.sql b/dinky-admin/src/main/resources/db/migration/mysql/V20241219.1.2.1__release.sql new file mode 100644 index 0000000000..41d128c34c --- /dev/null +++ b/dinky-admin/src/main/resources/db/migration/mysql/V20241219.1.2.1__release.sql @@ -0,0 +1,74 @@ +/* + * + * 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. + * + */ + + +SET NAMES Utf8mb4; +SET + FOREIGN_KEY_CHECKS = 0; + + +-- ---------------------------- +-- Table structure for dinky_approval +-- ---------------------------- + +CREATE TABLE IF NOT EXISTS `dinky_approval` +( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', + `task_id` int(11) NOT NULL COMMENT 'task id', + `tenant_id` int(11) NOT NULL default 1 COMMENT 'tenant id', + `previous_task_version` int(11) DEFAULT NULL COMMENT 'previous version of task', + `current_task_version` int(11) NOT NULL COMMENT 'current version to be reviewed of task', + `status` varchar(50) CHARACTER SET Utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'approval status', + `submitter` int(11) NOT NULL COMMENT 'submitter user id', + `submitter_comment` text DEFAULT NULL COMMENT 'submitter comment', + `reviewer` int(11) DEFAULT NULL COMMENT 'reviewer user id', + `reviewer_comment` text DEFAULT NULL COMMENT 'reviewer comment', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_tIme` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time', + PRIMARY KEY (`id`) USING BTREE, + INDEX `task_id_current_version_union_idx` (`task_id`, `current_task_version`) USING BTREE, + INDEX `submitter_tenant_id_union_idx` (`submitter`, `tenant_id`) USING BTREE, + INDEX `reviewer_tenant_id_union_idx` (`reviewer`, `tenant_id`) USING BTREE +) ENGINE = INNODB + AUTO_INCREMENT = 2 + CHARACTER SET = Utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = 'approval' + ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- approval menu +-- ---------------------------- + +INSERT INTO `dinky_sys_menu` (`id`, `parent_id`, `name`, `path`, `component`, `perms`, `icon`, `type`, `display`, + `order_num`, `create_time`, `update_time`, `note`) +VALUES (176, 4, '审批发布', '/auth/approval', './AuthCenter/Approval', 'auth:approval:operate', 'AuditOutlined', 'C', 0, 169, + '2024-12-10 12:13:00', '2024-12-10 12:13:00', null); + +insert into `dinky_sys_menu` (`id`, `parent_id`, `name`, `path`, `component`, `perms`, `icon`, `type`, `display`, + `order_num`, `create_time`, `update_time`, `note`) +values (177, 24, '审批配置', '/settings/globalsetting/approval', null, 'settings:globalsetting:approval', + 'SettingOutlined', 'F', 0, 170, '2024-12-30 23:45:30', '2024-12-30 23:45:30', null); + +insert into `dinky_sys_menu` (`id`, `parent_id`, `name`, `path`, `component`, `perms`, `icon`, `type`, `display`, + `order_num`, `create_time`, `update_time`, `note`) +values (178, 177, '编辑', '/settings/globalsetting/approval/edit', null, 'settings:globalsetting:approval:edit', + 'EditOutlined', 'F', 0, 171, '2024-12-30 23:45:30', '2024-12-30 23:45:30', null); + +SET + FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/dinky-admin/src/main/resources/db/migration/postgresql/V20241219.1.2.1__release.sql b/dinky-admin/src/main/resources/db/migration/postgresql/V20241219.1.2.1__release.sql new file mode 100644 index 0000000000..417d9e8dfd --- /dev/null +++ b/dinky-admin/src/main/resources/db/migration/postgresql/V20241219.1.2.1__release.sql @@ -0,0 +1,75 @@ +/* + * + * 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. + * + */ + + +-- ---------------------------- +-- Table structure for dinky_approval +-- ---------------------------- + +CREATE TABLE IF NOT EXISTS public.dinky_approval +( + id SERIAL PRIMARY KEY NOT NULL, + task_id INT NOT NULL, + tenant_id INT NOT NULL default 1, + previous_task_version INT NOT NULL, + current_task_version INT NOT NULL, + status VARCHAR(255) null, + submitter INT NOT NULL, + submitter_comment VARCHAR(255) null, + reviewer INT NULL, + reviewer_comment VARCHAR(255) null, + create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON COLUMN public.dinky_approval.id IS 'ID'; +COMMENT ON COLUMN public.dinky_approval.task_id IS 'name'; +COMMENT ON COLUMN public.dinky_approval.tenant_id IS 'tenant_id'; +COMMENT ON COLUMN public.dinky_approval.previous_task_version IS 'previous version of task'; +COMMENT ON COLUMN public.dinky_approval.current_task_version IS 'current version to be reviewed of task'; +COMMENT ON COLUMN public.dinky_approval.status IS 'approval status'; +COMMENT ON COLUMN public.dinky_approval.submitter IS 'submitter user id'; +COMMENT ON COLUMN public.dinky_approval.submitter_comment IS 'submitter comment'; +COMMENT ON COLUMN public.dinky_approval.reviewer IS 'reviewer user id'; +COMMENT ON COLUMN public.dinky_approval.reviewer_comment IS 'reviewer comment'; +COMMENT ON COLUMN public.dinky_approval.create_time IS 'create time'; +COMMENT ON COLUMN public.dinky_approval.update_tIme IS 'update time'; + +CREATE UNIQUE INDEX IF NOT EXISTS task_id_current_version_idx ON public.dinky_approval (task_id, current_task_version); +CREATE UNIQUE INDEX IF NOT EXISTS tenant_id_submitter_union_idx ON public.dinky_approval (submitter, tenant_id); +CREATE UNIQUE INDEX IF NOT EXISTS tenant_id_reviewer_union_idx ON public.dinky_approval (reviewer, tenant_id); + +-- ---------------------------- +-- approval menu +-- ---------------------------- + +INSERT INTO public.dinky_sys_menu(id, parent_id, name, path, component, perms, icon, type, display, order_num, + create_time, update_time, note) +VALUES (176, 4, '审批发布', '/auth/approval', './AuthCenter/Approval', 'auth:approval:operate', 'AuditOutlined', 'C', 0, 169, + '2024-12-10 12:13:00', '2024-12-10 12:13:00', null); + +insert into `dinky_sys_menu` (`id`, `parent_id`, `name`, `path`, `component`, `perms`, `icon`, `type`, `display`, + `order_num`, `create_time`, `update_time`, `note`) +values (177, 24, '审批配置', '/settings/globalsetting/approval', null, 'settings:globalsetting:approval', + 'SettingOutlined', 'F', 0, 170, '2024-12-30 23:45:30', '2024-12-30 23:45:30', null); + +insert into `dinky_sys_menu` (`id`, `parent_id`, `name`, `path`, `component`, `perms`, `icon`, `type`, `display`, + `order_num`, `create_time`, `update_time`, `note`) +values (178, 177, '编辑', '/settings/globalsetting/approval/edit', null, 'settings:globalsetting:approval:edit', + 'EditOutlined', 'F', 0, 171, '2024-12-30 23:45:30', '2024-12-30 23:45:30', null); diff --git a/dinky-admin/src/main/resources/mapper/ApprovalMapper.xml b/dinky-admin/src/main/resources/mapper/ApprovalMapper.xml new file mode 100644 index 0000000000..bf29aa67ae --- /dev/null +++ b/dinky-admin/src/main/resources/mapper/ApprovalMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + id, + task_id, + tenant_id, + previous_task_version, + current_task_version, + status, + submitter, + submitter_comment, + reviewer, + reviewer_comment, + create_time, + update_time + + + + + + \ No newline at end of file diff --git a/dinky-common/src/main/java/org/dinky/data/annotations/CheckTaskApproval.java b/dinky-common/src/main/java/org/dinky/data/annotations/CheckTaskApproval.java new file mode 100644 index 0000000000..e935b91cd9 --- /dev/null +++ b/dinky-common/src/main/java/org/dinky/data/annotations/CheckTaskApproval.java @@ -0,0 +1,37 @@ +/* + * + * 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.dinky.data.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface CheckTaskApproval { + Class checkParam(); + + String checkMethod() default "needApprove"; + + Class checkInterface(); +} diff --git a/dinky-common/src/main/java/org/dinky/data/enums/ApprovalEvent.java b/dinky-common/src/main/java/org/dinky/data/enums/ApprovalEvent.java new file mode 100644 index 0000000000..5bcdd61da8 --- /dev/null +++ b/dinky-common/src/main/java/org/dinky/data/enums/ApprovalEvent.java @@ -0,0 +1,38 @@ +/* + * + * 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.dinky.data.enums; + +public enum ApprovalEvent { + UNKNOWN("UNKNOWN"), + SUBMIT("SUBMIT"), + APPROVE("APPROVE"), + REJECT("REJECT"), + WITHDRAW("WITHDRAW"), + CANCEL("CANCEL"); + private final String value; + + ApprovalEvent(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/dinky-common/src/main/java/org/dinky/data/enums/ApprovalStatus.java b/dinky-common/src/main/java/org/dinky/data/enums/ApprovalStatus.java new file mode 100644 index 0000000000..1a777b0e35 --- /dev/null +++ b/dinky-common/src/main/java/org/dinky/data/enums/ApprovalStatus.java @@ -0,0 +1,57 @@ +/* + * + * 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.dinky.data.enums; + +import org.dinky.assertion.Asserts; + +import java.util.Arrays; + +public enum ApprovalStatus { + UNKNOWN("UNKNOWN"), + CREATED("CREATED"), + SUBMITTED("SUBMITTED"), + APPROVED("APPROVED"), + REJECTED("REJECTED"), + CANCELED("CANCELED"); + private final String value; + + ApprovalStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static ApprovalStatus fromValue(String value) { + return Arrays.stream(ApprovalStatus.values()) + .filter(type -> Asserts.isEqualsIgnoreCase(type.getValue(), value)) + .findFirst() + .orElse(ApprovalStatus.UNKNOWN); + } + + public static boolean isInProcess(ApprovalStatus status) { + return status.equals(ApprovalStatus.SUBMITTED); + } + + public boolean equalVal(String value) { + return this.value.equals(value); + } +} diff --git a/dinky-common/src/main/java/org/dinky/data/enums/Status.java b/dinky-common/src/main/java/org/dinky/data/enums/Status.java index f68b177cc2..0b1c1ed5e5 100644 --- a/dinky-common/src/main/java/org/dinky/data/enums/Status.java +++ b/dinky-common/src/main/java/org/dinky/data/enums/Status.java @@ -468,7 +468,18 @@ public enum Status { 204, "sys.flink.settings.flinkHistoryServerArchiveRefreshInterval"), SYS_FLINK_SETTINGS_FLINK_HISTORY_SERVER_ARCHIVE_REFRESH_INTERVAL_NOTE( 205, "sys.flink.settings.flinkHistoryServerArchiveRefreshInterval.note"), - ; + + /** + * approval + * */ + SYS_APPROVAL_SETTINGS_ENABLE_TASK_SUBMIT_REVIEW(206, "sys.approval.settings.enableTaskSubmitReview"), + SYS_APPROVAL_SETTINGS_ENABLE_TASK_SUBMIT_REVIEW_NOTE(207, "sys.approval.settings.enableTaskSubmitReview.note"), + SYS_APPROVAL_SETTINGS_ENFORCE_CROSS_REVIEW(208, "sys.approval.settings.enforceCrossReview"), + SYS_APPROVAL_SETTINGS_ENFORCE_CROSS_REVIEW_NOTE(209, "sys.approval.settings.enforceCrossReview.note"), + SYS_APPROVAL_SETTINGS_TASK_REVIEWER_ROLES(210, "sys.approval.settings.taskReviewerRoles"), + SYS_APPROVAL_SETTINGS_TASK_REVIEWER_ROLES_NOTE(211, "sys.approval.settings.taskReviewerRoles.note"), + SYS_APPROVAL_TASK_NOT_APPROVED(212, "sys.approval.taskNotApproved"), + SYS_APPROVAL_DUPLICATE_APPROVAL_IN_PROCESS(213, "sys.approval.duplicateInProcess"); private final int code; private final String key; diff --git a/dinky-common/src/main/java/org/dinky/data/model/SystemConfiguration.java b/dinky-common/src/main/java/org/dinky/data/model/SystemConfiguration.java index 85ec79ce52..6c44f691de 100644 --- a/dinky-common/src/main/java/org/dinky/data/model/SystemConfiguration.java +++ b/dinky-common/src/main/java/org/dinky/data/model/SystemConfiguration.java @@ -33,6 +33,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -326,6 +327,19 @@ public static Configuration.OptionBuilder key(Status status) { .booleanType() .defaultValue(true) .note(Status.SYS_RESOURCE_SETTINGS_PATH_STYLE_ACCESS_NOTE); + private final Configuration enableTaskSubmitReview = + key(Status.SYS_APPROVAL_SETTINGS_ENABLE_TASK_SUBMIT_REVIEW) + .booleanType() + .defaultValue(false) + .note(Status.SYS_APPROVAL_SETTINGS_ENABLE_TASK_SUBMIT_REVIEW_NOTE); + private final Configuration enforceCrossReview = key(Status.SYS_APPROVAL_SETTINGS_ENFORCE_CROSS_REVIEW) + .booleanType() + .defaultValue(true) + .note(Status.SYS_APPROVAL_SETTINGS_ENFORCE_CROSS_REVIEW_NOTE); + private final Configuration taskReviewerRoles = key(Status.SYS_APPROVAL_SETTINGS_TASK_REVIEWER_ROLES) + .stringType() + .defaultValue("SuperAdmin") + .note(Status.SYS_APPROVAL_SETTINGS_TASK_REVIEWER_ROLES_NOTE); /** * Initialize after spring bean startup @@ -454,4 +468,18 @@ public Map getFlinkHistoryServerConfiguration() { } return config; } + + public boolean enableTaskSubmitApprove() { + return enableTaskSubmitReview.getValue(); + } + + public boolean enforceCrossView() { + return enforceCrossReview.getValue(); + } + + public Set getReviewerRoles() { + return Arrays.stream(taskReviewerRoles.getValue().split(",")) + .map(String::trim) + .collect(Collectors.toSet()); + } } diff --git a/dinky-common/src/main/resources/i18n/messages_en_US.properties b/dinky-common/src/main/resources/i18n/messages_en_US.properties index f71482d07d..3c76818093 100644 --- a/dinky-common/src/main/resources/i18n/messages_en_US.properties +++ b/dinky-common/src/main/resources/i18n/messages_en_US.properties @@ -323,3 +323,13 @@ sys.flink.settings.flinkHistoryServerPort=Flink History Server Port sys.flink.settings.flinkHistoryServerPort.note=Flink History Server Port,For example, 8082, make sure that the port is not occupied sys.flink.settings.flinkHistoryServerArchiveRefreshInterval= Flink History Server refresh Interval sys.flink.settings.flinkHistoryServerArchiveRefreshInterval.note=For example, 10,000 refresh interval of the Flink History Server is refreshed every 10 seconds + +# approval +sys.approval.settings.enableTaskSubmitReview=Enable Task Submit Review +sys.approval.settings.enableTaskSubmitReview.note=Tasks will only be allowed to submit after approved when this option is enabled. +sys.approval.settings.enforceCrossReview=Enforce Cross Review +sys.approval.settings.enforceCrossReview.note=Submitter of an approval are not allowed to be reviewer at the same time when this option is enabled. +sys.approval.settings.taskReviewerRoles=Reviewer Role Codes +sys.approval.settings.taskReviewerRoles.note=Roles who can review tasks, different role codes should be divided by comma. For example: SuperAdmin,Reviewer +sys.approval.taskNotApproved=Current task is not published or published version is not approved, please try again after task being published and approved +sys.approval.duplicateInProcess=Already have an approval in process, please do not submit again \ No newline at end of file diff --git a/dinky-common/src/main/resources/i18n/messages_zh_CN.properties b/dinky-common/src/main/resources/i18n/messages_zh_CN.properties index 0902bca283..72f82d4770 100644 --- a/dinky-common/src/main/resources/i18n/messages_zh_CN.properties +++ b/dinky-common/src/main/resources/i18n/messages_zh_CN.properties @@ -325,3 +325,12 @@ sys.flink.settings.flinkHistoryServerPort=Flink History Server 端口 sys.flink.settings.flinkHistoryServerPort.note=Flink History Server 端口,例如:8082,确保端口没有被占用 sys.flink.settings.flinkHistoryServerArchiveRefreshInterval= Flink History Server 刷新间隔 sys.flink.settings.flinkHistoryServerArchiveRefreshInterval.note=Flink History Server 刷新间隔,单位:毫秒,例如:10000,表示每隔10秒刷新一次 +# approval +sys.approval.settings.enableTaskSubmitReview=开启作业上线审核 +sys.approval.settings.enableTaskSubmitReview.note=开启后,作业只有在通过审核之后才能够提交上线,默认关闭 +sys.approval.settings.enforceCrossReview=强制交叉审核 +sys.approval.settings.enforceCrossReview.note=开启强制交叉审核后,不允许提交人审批自己的作业 +sys.approval.settings.taskReviewerRoles=具有审批权限的角色编码 +sys.approval.settings.taskReviewerRoles.note=具有审批权限的角色编码,多个角色用英文逗号隔开,例如:SuperAdmin,Reviewer +sys.approval.taskNotApproved=当前作业未发布或发布版本未通过审核,不允许运行,请在任务发布且发布版本通过审核后重试 +sys.approval.duplicateInProcess=存在仍在进行的审批流程,请勿重复提交 diff --git a/dinky-web/config/routes.ts b/dinky-web/config/routes.ts index 7fa55a7b7f..ff29d9fd1e 100644 --- a/dinky-web/config/routes.ts +++ b/dinky-web/config/routes.ts @@ -249,6 +249,12 @@ export default [ name: 'token', icon: 'SecurityScanOutlined', component: './AuthCenter/Token' + }, + { + path: '/auth/approval', + name: 'approval', + icon: 'AuditOutlined', + component: './AuthCenter/Approval' } ] }, diff --git a/dinky-web/src/components/Icons/CustomIcons.tsx b/dinky-web/src/components/Icons/CustomIcons.tsx index 7bb597c2db..3e2089e95f 100644 --- a/dinky-web/src/components/Icons/CustomIcons.tsx +++ b/dinky-web/src/components/Icons/CustomIcons.tsx @@ -657,6 +657,33 @@ export const ResourceIcon = (props: any) => { ); }; +export const ApprovalIcon = (props: any) => { + const size = props.size || defaultSvgSize; + + return ( + <> + ( + + + + )} + /> + + ); +}; + export const MenuIcon = (props: any) => { const size = props.size || defaultSvgSize; diff --git a/dinky-web/src/locales/en-US/menu.ts b/dinky-web/src/locales/en-US/menu.ts index 28f609079c..f9a418db45 100644 --- a/dinky-web/src/locales/en-US/menu.ts +++ b/dinky-web/src/locales/en-US/menu.ts @@ -78,6 +78,7 @@ export default { 'menu.auth.namespace': 'NameSpace', 'menu.auth.tenant': 'Tenant', 'menu.auth.token': 'Token', + 'menu.auth.approval': 'Approval', 'menu.settings': 'Setting Center', 'menu.settings.globalsetting': 'Global Settings', 'menu.settings.systemlog': 'System Log', @@ -115,5 +116,9 @@ export default { 'menu.datastudio.tool.text-comparison': 'Text Comparison', 'menu.datastudio.tool.jsonToSql': 'JSON TO Flink-SQL', 'menu.datastudio.task.baseConfig': 'Basic Configuration', - 'menu.datastudio.task.previewConfig': 'Preview Configuration' + 'menu.datastudio.task.previewConfig': 'Preview Configuration', + + 'menu.approval': 'Approval Center', + 'menu.approval.taskApproval': 'Task Approval', + 'menu.approval.submitApproval': 'Submit Approval' }; diff --git a/dinky-web/src/locales/en-US/pages.ts b/dinky-web/src/locales/en-US/pages.ts index 43a92710c1..7ca95718a0 100644 --- a/dinky-web/src/locales/en-US/pages.ts +++ b/dinky-web/src/locales/en-US/pages.ts @@ -1267,6 +1267,8 @@ export default { 'You can enter your username/nickname for search, support fuzzy queries, enter keywords and press enter to complete the search', 'sys.ldap.settings.loadable': 'Whether it can be imported', 'sys.setting.ingress': 'Ingress configuration', + 'sys.setting.approval': 'Approval Configuration', + 'sys.setting.approval.tooltip': 'Approval Configuration for task submit', /** * * tenant @@ -1419,5 +1421,34 @@ export default { 'datastudio.toolbar.rightClick.hideToolbarDesc': 'hides the toolbar window name', 'datastudio.toolbar.rightClick.showToolbarDesc': 'displays the toolbar window name', 'datastudio.toolbar.rightClick.closeCompact': 'turn off compact mode', - 'datastudio.toolbar.rightClick.openCompact': 'turn on compact mode' + 'datastudio.toolbar.rightClick.openCompact': 'turn on compact mode', + + 'approval.dinky.not.open': 'Dinky approval is not enabled, please go to the Setting Center -> Approval Configuration switch to open', + 'approval.reviewList': 'Review List', + 'approval.submitList': 'Submit List', + 'approval.id': 'Approval Id', + 'approval.taskId': 'Task Id', + 'approval.previousTaskVersion': 'Previous Task Version Id', + 'approval.currentTaskVersion': 'Current Task Version Id', + 'approval.status': 'Approval Status', + 'approval.submitterName': 'Submitter Name', + 'approval.submitterComment': 'Submitter Comment', + 'approval.reviewerName': 'Reviewer Name', + 'approval.reviewerComment': 'Reviewer Comment', + 'approval.status.created': 'CREATED', + 'approval.status.withdrawn': 'WITHDRAWN', + 'approval.status.submitted': 'SUBMITTED', + 'approval.status.approved': 'APPROVED', + 'approval.status.rejected': 'REJECTED', + 'approval.status.canceled': 'CANCELED', + 'approval.operation.create': 'Create Approval', + 'approval.operation.withdraw': 'Withdraw Approval', + 'approval.operation.submit': 'Submit Approval', + 'approval.operation.approve': 'Approve Approval', + 'approval.operation,reject': 'Reject Approval', + 'approval.operation.cancel': 'Cancel Approval', + 'approval.reviewer.required': 'Please select a reviewer', + 'approval.submit.comment': 'Submit Remark', + 'approval.review.comment': 'Review Comment', + 'approval.taskInfo': 'Task Info' }; diff --git a/dinky-web/src/locales/zh-CN/menu.ts b/dinky-web/src/locales/zh-CN/menu.ts index 490106f0dc..9661d2fc4c 100644 --- a/dinky-web/src/locales/zh-CN/menu.ts +++ b/dinky-web/src/locales/zh-CN/menu.ts @@ -78,6 +78,7 @@ export default { 'menu.auth.namespace': '命名空间', 'menu.auth.tenant': '租户', 'menu.auth.token': '令牌', + 'menu.auth.approval': '审核发布', 'menu.settings': '配置中心', 'menu.settings.globalsetting': '全局配置', 'menu.settings.systemlog': '系统日志', @@ -114,5 +115,9 @@ export default { 'menu.datastudio.tool.text-comparison': '文本比对', 'menu.datastudio.tool.jsonToSql': 'JSON转Flink-SQL', 'menu.datastudio.task.baseConfig': '基础配置', - 'menu.datastudio.task.previewConfig': '预览配置' + 'menu.datastudio.task.previewConfig': '预览配置', + + 'menu.approval': '审批中心', + 'menu.approval.taskApproval': '任务审批', + 'menu.approval.submitApproval': '提交审核' }; diff --git a/dinky-web/src/locales/zh-CN/pages.ts b/dinky-web/src/locales/zh-CN/pages.ts index cdfc6065ce..65c9608f1c 100644 --- a/dinky-web/src/locales/zh-CN/pages.ts +++ b/dinky-web/src/locales/zh-CN/pages.ts @@ -1180,6 +1180,8 @@ export default { 'sys.ldap.settings.keyword': '可输入用户名/昵称进行搜索,支持模糊查询,输入关键词后回车即可', 'sys.ldap.settings.loadable': '是否可以导入', 'sys.setting.ingress': 'Ingress 配置', + 'sys.setting.approval': '审批配置', + 'sys.setting.approval.tooltip': '作业上线的审批配置', /** * * tenant @@ -1330,5 +1332,35 @@ export default { 'datastudio.toolbar.rightClick.hideToolbarDesc': '隐藏工具栏窗口名称', 'datastudio.toolbar.rightClick.showToolbarDesc': '显示工具栏窗口名称', 'datastudio.toolbar.rightClick.closeCompact': '关闭紧凑模式', - 'datastudio.toolbar.rightClick.openCompact': '打开紧凑模式' + 'datastudio.toolbar.rightClick.openCompact': '打开紧凑模式', + + 'approval.dinky.not.open': '暂未开启审批功能,请前往 配置中心 -> 审批配置 进行配置', + 'approval.reviewList': '审批列表', + 'approval.submitList': '申请列表', + 'approval.id': '工单id', + 'approval.taskId': '任务id', + 'approval.previousTaskVersion': '线上版本', + 'approval.currentTaskVersion': '提交审核版本', + 'approval.status': '工单状态', + 'approval.submitterName': '提交人', + 'approval.submitterComment': '上线备注', + 'approval.reviewer': '审核人id', + 'approval.reviewerName': '审核人', + 'approval.reviewerComment': '审核备注', + 'approval.status.created': '已创建', + 'approval.status.withdrawn': '已撤回', + 'approval.status.submitted': '已提交', + 'approval.status.approved': '已通过', + 'approval.status.rejected': '已驳回', + 'approval.status.canceled': '已取消', + 'approval.operation.create': '创建审批', + 'approval.operation.withdraw': '撤回审批', + 'approval.operation.submit': '提交审批', + 'approval.operation.approve': '通过审批', + 'approval.operation.reject': '驳回审批', + 'approval.operation.cancel': '取消审批', + 'approval.reviewer.required': '请选择一位审批人', + 'approval.submit.comment': '上线说明', + 'approval.review.comment': '审批意见', + 'approval.taskInfo': '上线任务详情' }; diff --git a/dinky-web/src/pages/AuthCenter/Approval/components/ApprovalModal/index.tsx b/dinky-web/src/pages/AuthCenter/Approval/components/ApprovalModal/index.tsx new file mode 100644 index 0000000000..ce7d51a3d1 --- /dev/null +++ b/dinky-web/src/pages/AuthCenter/Approval/components/ApprovalModal/index.tsx @@ -0,0 +1,81 @@ +/* + * + * 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 { ModalForm, ProFormSelect, ProFormTextArea } from "@ant-design/pro-components"; +import React from "react"; +import { OperationType } from "@/types/AuthCenter/data.d"; +import { l } from "@/utils/intl"; +import { API_CONSTANTS } from "@/services/endpoints"; +import { getTenantByLocalStorage } from "@/utils/function"; +import { getData } from "@/services/api"; + +type ApprovalModelProps = { + open: boolean, + title: string, + activeId: number, + operationType: OperationType, + onOpenChange: (open: boolean) => void; + handleSubmit: (record) => void; +}; + +const ApprovalModal: React.FC = (props) => { + + const getReviewerList = async () => { + const reviewers = (await getData(API_CONSTANTS.GET_REVIEWERS, {tenantId: getTenantByLocalStorage()})).data; + return reviewers.map((t) => ({label: t.username, value: t.id})); + } + + const approvalRender = () => { + if (props.operationType == OperationType.SUBMIT) { + return ( + <> + getReviewerList()} + placeholder={l('approval.reviewer.required')} + rules={[{required: true}]} + /> + + + ) + } else { + return ( + <> + + + ) + } + }; + return ( + { + record.id = props.activeId + await props.handleSubmit(record); + }} + > + {approvalRender()} + + ); +}; + +export default ApprovalModal; diff --git a/dinky-web/src/pages/AuthCenter/Approval/components/ApprovalTable/index.tsx b/dinky-web/src/pages/AuthCenter/Approval/components/ApprovalTable/index.tsx new file mode 100644 index 0000000000..ffb44e8153 --- /dev/null +++ b/dinky-web/src/pages/AuthCenter/Approval/components/ApprovalTable/index.tsx @@ -0,0 +1,419 @@ +/* + * + * 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 { ApprovalBasicInfo, OperationStatus, OperationType } from "@/types/AuthCenter/data.d"; +import React, { useRef, useState } from "react"; +import { InitApprovalList } from "@/types/AuthCenter/init.d"; +import { ActionType, ProColumns } from "@ant-design/pro-table"; +import { Button, Flex, Tag } from "antd"; +import { l } from "@/utils/intl"; +import { ProTable } from "@ant-design/pro-components"; +import { queryList } from "@/services/api"; +import { API_CONSTANTS } from "@/services/endpoints"; +import { handleOption, queryDataByParams } from "@/services/BusinessCrud"; +import { getTaskDetails } from "@/pages/DataStudio/service"; +import { TaskState } from "@/pages/DataStudio/type"; +import TaskInfoModal from "@/pages/AuthCenter/Approval/components/TaskInfoModal"; +import ApprovalModal from "@/pages/AuthCenter/Approval/components/ApprovalModal"; +import { useAsyncEffect } from "ahooks"; +import { getTenantByLocalStorage } from "@/utils/function"; +import { ApprovalListState } from "@/types/AuthCenter/state.d"; + + +type UserFormProps = { + tableType: 'review' | 'submit' +} + +const ApprovalTable: React.FC = (props) => { + + // approval list + const [approvalListState, setApprovalListState] = useState(InitApprovalList); + + // users + const [userMap, setUserMap] = useState>(); + + // active approval state + const [activeApprovalState, setActiveApprovalState] = useState<{ + activeId: number; + taskInfoOpen: boolean; + taskBasicInfo: TaskState | undefined; + preVersionStatement: string; + curVersionStatement: string; + }>({activeId: -1, taskInfoOpen: false, taskBasicInfo: undefined, preVersionStatement: '', curVersionStatement: ''}); + + // operation modal states + const [operationState, setOperationState] = useState<{ + operationType: OperationType; + operationModalOpen: boolean; + operationDesc: string; + }>({operationType: OperationType.SUBMIT, operationModalOpen: false, operationDesc: ''}); + + const actionRef = useRef(); // table action + + // get user list + useAsyncEffect(async () => { + const tempUserMap: Map = new Map(); + const usersRes = await queryDataByParams(API_CONSTANTS.GET_USER_LIST_BY_TENANTID, {id: getTenantByLocalStorage()}); + usersRes.users.forEach((user) => { + tempUserMap.set(user.id, user.username); + }); + setUserMap(tempUserMap); + actionRef.current?.reload(); + }, []); + + const executeAndCallbackRefresh = async (callback: () => void) => { + setApprovalListState((prevState) => ({...prevState, loading: true})); + await callback(); + actionRef.current?.reload(); + setApprovalListState((prevState) => ({...prevState, loading: false})); + }; + + const handleOperationButtonClick = (operation: OperationType, entity: ApprovalBasicInfo) => { + setActiveApprovalState((prevState) => ({...prevState, activeId: entity.id})); + switch (operation) { + case OperationType.SUBMIT: + setOperationState((prevState) => ({ + ...prevState, + operationDesc: l('approval.operation.submit'), + operationType: operation, + operationModalOpen: true + })); + break; + case OperationType.REJECT: + setOperationState((prevState) => ({ + ...prevState, + operationDesc: l('approval.operation.reject'), + operationType: operation, + operationModalOpen: true + })); + break; + case OperationType.APPROVE: + setOperationState((prevState) => ({ + ...prevState, + operationDesc: l('approval.operation.approve'), + operationType: operation, + operationModalOpen: true + })); + break; + } + }; + + const handleWithdraw = async (entity: ApprovalBasicInfo) => { + await executeAndCallbackRefresh(async () => { + await handleOption(API_CONSTANTS.APPROVAL_WITHDRAW, l('approval.operation.withdraw'), entity); + }); + }; + + const handleCancel = async (entity: ApprovalBasicInfo) => { + await executeAndCallbackRefresh(async () => { + await handleOption(API_CONSTANTS.APPROVAL_CANCEL, l('approval.operation.cancel'), entity); + }); + }; + + const queryApproval = async (params, sorter, filter: any) => { + const queryRes = await queryList(props.tableType === 'review' ? API_CONSTANTS.GET_REVIEW_REQUIRED_APPROVAL : API_CONSTANTS.GET_SUBMITTED_APPROVAL, { + ...params, + sorter, + filter + }); + const convertedQueryRes = []; + queryRes.data.forEach((approval) => { + convertedQueryRes.push({ + ...approval, + submitterName: userMap?.get(approval.submitter), + reviewerName: userMap?.get(approval.reviewer) + }) + }); + return {...queryRes, data: convertedQueryRes}; + } + + const queryTaskDiffInfo = async (taskId: number, preVersionId: number, curVersionId: number) => { + const taskInfo = await getTaskDetails(taskId); + setActiveApprovalState((prevState) => ({ + ...prevState, + taskBasicInfo: taskInfo, + preVersionStatement: '', + curVersionStatement: '' + })); + const versions = await queryDataByParams(API_CONSTANTS.GET_JOB_VERSION, {taskId: taskId}); + + versions.forEach((version) => { + if (version.versionId == preVersionId) { + setActiveApprovalState((prevState) => ({ + ...prevState, + taskBasicInfo: taskInfo, + preVersionStatement: version.statement + })); + } + if (version.versionId == curVersionId) { + setActiveApprovalState((prevState) => ({ + ...prevState, + taskBasicInfo: taskInfo, + curVersionStatement: version.statement + })); + } + }) + } + + const handleApprovalSubmit = async (record) => { + await executeAndCallbackRefresh(async () => { + switch (operationState.operationType) { + case OperationType.SUBMIT: + await handleOption(API_CONSTANTS.APPROVAL_SUBMIT, l('approval.operation.submit'), record); + break; + case OperationType.REJECT: + await handleOption(API_CONSTANTS.APPROVAL_REJECT, l('approval.operation.reject'), record); + break; + case OperationType.APPROVE: + await handleOption(API_CONSTANTS.APPROVAL_APPROVE, l('approval.operation.approve'), record); + break; + } + }); + setOperationState((prevState) => ({...prevState, operationModalOpen: false})); + } + + /** + * render operation based on current state + * @param entity entity + */ + const renderOperation = (entity: ApprovalBasicInfo) => { + const buttons = []; + // review list: approve and reject, submit list: submit withdraw cancel + switch (entity.status) { + case OperationStatus.CREATED: + if (props.tableType == 'submit') { + buttons.push( + + ); + buttons.push( + + ); + } + break; + case OperationStatus.SUBMITTED: + if (props.tableType == 'review') { + buttons.push( + + ); + buttons.push( + + ); + } else { + buttons.push( + + ); + } + break; + } + return ( + <> + + {buttons} + + + ) + }; + + const renderInfo = (entity: ApprovalBasicInfo) => { + return ( + <> + + + ) + } + + /** + * status color + */ + const statusNum = { + CREATED: { + text: {l('approval.status.created')} + }, + SUBMITTED: { + text: {l('approval.status.submitted')} + }, + APPROVED: { + text: {l('approval.status.approved')} + }, + REJECTED: { + text: {l('approval.status.rejected')} + }, + CANCELED: { + text: {l('approval.status.canceled')} + }, + }; + + const approvalColumns: ProColumns[] = [ + { + title: l('approval.id'), + dataIndex: 'id', + key: 'id' + }, + { + title: l('approval.taskId'), + dataIndex: 'taskId' + }, + { + title: l('approval.previousTaskVersion'), + dataIndex: 'previousTaskVersion', + hideInSearch: true + }, + { + title: l('approval.currentTaskVersion'), + dataIndex: 'currentTaskVersion', + hideInSearch: true + }, + { + title: l('approval.taskInfo'), + valueType: 'option', + render: (_: any, record: ApprovalBasicInfo) => renderInfo(record) + }, + { + title: l('approval.status'), + dataIndex: 'status', + valueEnum: statusNum + }, + { + title: l('approval.submitterName'), + dataIndex: 'submitterName', + hideInSearch: true + }, + { + title: l('approval.submitterComment'), + dataIndex: 'submitterComment', + hideInSearch: true + }, + { + title: l('approval.reviewerName'), + dataIndex: 'reviewerName', + hideInSearch: true + }, + { + title: l('approval.reviewerComment'), + dataIndex: 'reviewerComment', + hideInSearch: true + }, + { + title: l('global.table.createTime'), + dataIndex: 'createTime', + hideInSearch: true, + sorter: true + }, + { + title: l('global.table.updateTime'), + dataIndex: 'updateTime', + hideInSearch: true + }, + { + title: l('global.table.operate'), + valueType: 'option', + width: '12%', + fixed: 'right', + render: (_: any, record: ApprovalBasicInfo) => renderOperation(record) + } + ]; + + return ( + <> + { + setOperationState(prevState => ({...prevState, operationModalOpen: open})) + }} + title={operationState.operationDesc} + activeId={activeApprovalState.activeId} + operationType={operationState.operationType} + handleSubmit={handleApprovalSubmit} + /> + { + setActiveApprovalState(prevState => ({...prevState, taskInfoOpen: false})) + }} + taskInfo={activeApprovalState.taskBasicInfo} + preVersionStatement={activeApprovalState.preVersionStatement} + curVersionStatement={activeApprovalState.curVersionStatement} + /> + + search={{filterType: 'query'}} + pagination={{pageSize: 20, size: 'small'}} + options={false} + rowKey={(record) => record.id} + loading={approvalListState.loading} + columns={approvalColumns} + request={queryApproval} + actionRef={actionRef} + /> + + ); +} + +export default ApprovalTable; + + diff --git a/dinky-web/src/pages/AuthCenter/Approval/components/TaskInfoModal/index.tsx b/dinky-web/src/pages/AuthCenter/Approval/components/TaskInfoModal/index.tsx new file mode 100644 index 0000000000..477ebd1cd9 --- /dev/null +++ b/dinky-web/src/pages/AuthCenter/Approval/components/TaskInfoModal/index.tsx @@ -0,0 +1,131 @@ +/* + * + * 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 { Col, Descriptions, DescriptionsProps, Modal, Row, Tabs, Typography } from "antd"; +import { l } from "@/utils/intl"; +import styles from "@/pages/DataStudio/CenterTabContent/index.less"; +import { DiffEditor } from "@monaco-editor/react"; +import { DIFF_EDITOR_PARAMS } from "@/pages/DataStudio/CenterTabContent/SqlTask/constants"; +import { LoadCustomEditorLanguage } from "@/components/CustomEditor/languages"; +import { convertCodeEditTheme } from "@/utils/function"; +import React from "react"; +import { TaskState } from "@/pages/DataStudio/type"; + +type TaskInfoProps = { + open: boolean; + onCancel: () => void; + taskInfo: TaskState | undefined; + preVersionStatement: string; + curVersionStatement: string; +} + +const {Text, Link} = Typography; +const TaskInfoModal = (props: TaskInfoProps) => { + const renderTaskInfo = () => { + const items: DescriptionsProps['item'] = [ + { + key: '1', + label: l('pages.datastudio.label.jobInfo.id'), + children:

{props.taskInfo?.taskId}

+ }, + { + key: '2', + label: l('pages.datastudio.label.jobInfo.name'), + children:

{props.taskInfo?.name}

+ }, + { + key: '3', + label: l('pages.datastudio.label.jobInfo.dialect'), + children:

{props.taskInfo?.dialect}

+ }, + { + key: '4', + label: l('pages.datastudio.label.jobConfig.flinksql.env'), + children:

{props.taskInfo?.envId}

+ }, + { + key: '5', + label: l('pages.datastudio.label.jobInfo.firstLevelOwner'), + children:

{props.taskInfo?.firstLevelOwner}

+ }, + ]; + + return ( + <> + + + ); + } + + const renderVersionCompare = () => { + return ( + <> + + + ) + }; + + // Render the statement diff section + const renderStatementDiff = () => { + return ( + <> +
+ + + {l('approval.previousTaskVersion')} + + + {l('approval.currentTaskVersion')} + + + LoadCustomEditorLanguage(monaco.languages, monaco.editor)} + original={props.preVersionStatement} + modified={props.curVersionStatement} + theme={convertCodeEditTheme()} + /> +
+ + ); + }; + + return ( + <> + + {renderTaskInfo()} + {renderVersionCompare()} + + + ) +} + +export default TaskInfoModal; diff --git a/dinky-web/src/pages/AuthCenter/Approval/index.tsx b/dinky-web/src/pages/AuthCenter/Approval/index.tsx new file mode 100644 index 0000000000..a2e7228fe3 --- /dev/null +++ b/dinky-web/src/pages/AuthCenter/Approval/index.tsx @@ -0,0 +1,107 @@ +/* + * + * 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 React, { useState } from "react"; +import { Alert, Space } from "antd"; +import { l } from "@/utils/intl"; +import useHookRequest from "@/hooks/useHookRequest"; +import { getAllConfig } from "@/pages/Metrics/service"; +import SlowlyAppear from "@/components/Animation/SlowlyAppear"; +import { PageContainer, ProCard } from "@ant-design/pro-components"; +import ApprovalTable from "@/pages/AuthCenter/Approval/components/ApprovalTable"; + + +const ApprovalFormList: React.FC = () => { + const [activeKey, setActiveKey] = useState('reviewList'); + const [tabs, setTabs] = useState([]); + + const tabList = [ + { + key: 'reviewList', + label: ( + + {l('approval.reviewList')} + + + ), + children: + }, + { + key: 'submitList', + label: ( + + {l('approval.submitList')} + + + ), + children: + } + ]; + + const showServer = useHookRequest(getAllConfig, { + defaultParams: [], + onSuccess: (res: any) => { + for (const config of res.approval) { + if (config.key === 'sys.approval.settings.enableTaskSubmitReview') { + if (config.value) { + setTabs(tabList as []); + return true; + } + } + } + return false; + } + }); + + /** + * render + */ + return ( + + + ) + } + title={false} + > + setActiveKey(key), + items: tabs + }} + /> + + + ); +}; + +export default ApprovalFormList; + + diff --git a/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx b/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx index 9a16a0554e..e0d086c486 100644 --- a/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx +++ b/dinky-web/src/pages/DataStudio/CenterTabContent/SqlTask/index.tsx @@ -26,6 +26,7 @@ import { Monaco } from '@monaco-editor/react'; import { Panel, PanelGroup } from 'react-resizable-panels'; import { ApartmentOutlined, + AuditOutlined, BugOutlined, CaretRightOutlined, ClearOutlined, @@ -84,6 +85,7 @@ import { DataStudioActionType } from '@/pages/DataStudio/data.d'; import { getDataByParams, handleOption, + handlePutDataByParams, handlePutDataJson, queryDataByParams } from '@/services/BusinessCrud'; @@ -109,6 +111,9 @@ import { DolphinTaskMinInfo } from '@/types/Studio/data'; import PushDolphin from '@/pages/DataStudio/CenterTabContent/SqlTask/PushDolphin'; +import ApprovalModal from "@/pages/AuthCenter/Approval/components/ApprovalModal"; +import { OperationType } from "@/types/AuthCenter/data.d"; +import { getAllConfig } from "@/pages/Metrics/service"; export type FlinkSqlProps = { showDesc: boolean; @@ -215,6 +220,16 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { formValuesInfo: {} }); + const [approvalState, setApprovalState] = useState<{ + enableApproval: boolean, + openSubmitModal: boolean; + currentApprovalId: number; + }>({ + enableApproval: false, + openSubmitModal: false, + currentApprovalId: -1 + }) + useEffect(() => { if (sqlForm.enable) { setSqlForm((prevState) => ({ @@ -265,6 +280,17 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { } } } + // check approval config + const allConfig = await getAllConfig(); + for (const config of allConfig.data.approval) { + if (config.key === 'sys.approval.settings.enableTaskSubmitReview') { + if (config.value) { + // show approval submit button + setApprovalState(prevState => ({...prevState, enableApproval: true})); + } + } + } + setLoading(false); }, []); @@ -723,6 +749,25 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { await handlePushDolphinCancel(); }; + const handleOpenApprovalModal = async () => { + // publish first + if (JOB_LIFE_CYCLE.PUBLISH != currentState.step) { + await handleChangeJobLife(); + } + // create approval + const res = await handlePutDataByParams( + API_CONSTANTS.TASK_APPROVAL_CREATE, + l('approval.operation.create'), + { taskId:currentState.taskId } + ); + // open submit modal + setApprovalState((prevState) => ({...prevState, currentApprovalId: res.data.id})); + handleApprovalModalOpenChange(true); + } + + const handleApprovalModalOpenChange = (open: boolean) => { + setApprovalState((prevState) => ({...prevState, openSubmitModal: open})); + }; return ( { fileName={currentState.name} onUse={updateTask} /> + { + await handleOption(API_CONSTANTS.APPROVAL_SUBMIT, l('approval.operation.submit'), record); + setApprovalState(prevState => ({...prevState, openSubmitModal: false})) + }} + /> { }} onClick={handlePushDolphinOpen} /> + } + onClick={handleOpenApprovalModal} + /> diff --git a/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/ApprovalConfig/index.tsx b/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/ApprovalConfig/index.tsx new file mode 100644 index 0000000000..f12b7f51e5 --- /dev/null +++ b/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/ApprovalConfig/index.tsx @@ -0,0 +1,55 @@ +/* + * + * 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 GeneralConfig from '@/pages/SettingCenter/GlobalSetting/SettingOverView/GeneralConfig'; +import { BaseConfigProperties } from '@/types/SettingCenter/data'; +import { l } from '@/utils/intl'; +import { Tag } from 'antd'; +import React from 'react'; + +interface ApprovalConfigProps { + data: BaseConfigProperties[]; + onSave: (data: BaseConfigProperties) => void; + auth: string; +} + +export const ApprovalConfig = ({ data, onSave, auth }: ApprovalConfigProps) => { + const [loading, setLoading] = React.useState(false); + + const onSaveHandler = async (data: BaseConfigProperties) => { + setLoading(true); + await onSave(data); + setLoading(false); + }; + return ( + <> + + {l('sys.setting.tag.extend')} + + } + data={data} + /> + + ); +}; diff --git a/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/constants.ts b/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/constants.ts index 89ab2651d7..2616ef28ec 100644 --- a/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/constants.ts +++ b/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/constants.ts @@ -25,7 +25,8 @@ export enum SettingConfigKeyEnum { LDAP = 'LDAP', METRIC = 'Metric', RESOURCE = 'Resource', - ENV = 'Env' + ENV = 'Env', + APPROVAL = 'Approval' } export enum ButtonFrontendType { diff --git a/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/index.tsx b/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/index.tsx index fa08e35ebf..cb32f9b7ff 100644 --- a/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/index.tsx +++ b/dinky-web/src/pages/SettingCenter/GlobalSetting/SettingOverView/index.tsx @@ -25,7 +25,8 @@ import { LDAPIcon, MavenIcon, MetricsIcon, - ResourceIcon + ResourceIcon, + ApprovalIcon } from '@/components/Icons/CustomIcons'; import { TagAlignCenter } from '@/components/StyledComponents'; import { AuthorizedObject, useAccess } from '@/hooks/useAccess'; @@ -45,6 +46,7 @@ import { BaseConfigProperties, Settings } from '@/types/SettingCenter/data'; import { l } from '@/utils/intl'; import { ProCard } from '@ant-design/pro-components'; import { memo, useEffect, useState } from 'react'; +import { ApprovalConfig } from "@/pages/SettingCenter/GlobalSetting/SettingOverView/ApprovalConfig"; const imgSize = 25; @@ -60,7 +62,8 @@ const SettingOverView = () => { maven: [], ldap: [], metrics: [], - resource: [] + resource: [], + approval: [] }); const fetchData = async () => { @@ -104,7 +107,8 @@ const SettingOverView = () => { dolphinscheduler: dsConfig, ldap: ldapConfig, metrics: metricsConfig, - resource: resourceConfig + resource: resourceConfig, + approval: approvalConfig } = data; return [ @@ -226,6 +230,23 @@ const SettingOverView = () => { /> ), path: PermissionConstants.SETTING_GLOBAL_RESOURCE + }, + { + key: SettingConfigKeyEnum.APPROVAL, + label: ( + + + {l('sys.setting.approval')} + + ), + children: ( + + ), + path: PermissionConstants.SETTING_GLOBAL_APPROVAL } ]; }; diff --git a/dinky-web/src/services/endpoints.tsx b/dinky-web/src/services/endpoints.tsx index 3c0fa3cb06..5a89046467 100644 --- a/dinky-web/src/services/endpoints.tsx +++ b/dinky-web/src/services/endpoints.tsx @@ -304,5 +304,16 @@ export enum API_CONSTANTS { FLINK_CONF_CONFIG_OPTIONS = '/api/flinkConf/configOptions', // ------------------------------------ suggestion ------------------------------------ - SUGGESTION_QUERY_ALL_SUGGESTIONS = '/api/suggestion/queryAllSuggestions' + SUGGESTION_QUERY_ALL_SUGGESTIONS = '/api/suggestion/queryAllSuggestions', + + // ------------------------------------ approval ------------------------------------ + TASK_APPROVAL_CREATE = '/api/approval/createTaskApproval', + GET_REVIEWERS = '/api/approval/getReviewers', + APPROVAL_SUBMIT = '/api/approval/submit', + APPROVAL_REJECT = '/api/approval/reject', + APPROVAL_APPROVE = '/api/approval/approve', + APPROVAL_WITHDRAW = '/api/approval/withdraw', + APPROVAL_CANCEL = '/api/approval/cancel', + GET_SUBMITTED_APPROVAL = '/api/approval/getSubmittedApproval', + GET_REVIEW_REQUIRED_APPROVAL = '/api/approval/getApprovalToBeReviewed' } diff --git a/dinky-web/src/types/AuthCenter/data.d.ts b/dinky-web/src/types/AuthCenter/data.d.ts index e9e323390d..b71a0864f7 100644 --- a/dinky-web/src/types/AuthCenter/data.d.ts +++ b/dinky-web/src/types/AuthCenter/data.d.ts @@ -142,3 +142,36 @@ export interface SysToken extends ExcludeNameAndEnableColumns { creator: number; updator: number; } + +export type ApprovalBasicInfo = { + id: number; + taskId: number; + previousTaskVersion: number; + currentTaskVersion: number; + status: OperationStatus; + submitterName: string; + submitterComment: string; + reviewerName: string; + reviewerComment: string; + createTime: string; + updateTime: string; +}; + +export enum OperationType { + UNKNOWN = 'UNKNOWN', + CREATE = 'CREATE', + SUBMIT = 'SUBMIT', + WITHDRAW = 'WITHDRAW', + APPROVE = 'APPROVE', + REJECT = 'REJECTED', + CANCEL = 'CANCEL' +} + +export enum OperationStatus { + UNKNOWN = 'UNKNOWN', + CREATED = 'CREATED', + SUBMITTED = 'SUBMITTED', + APPROVED = 'APPROVED', + REJECTED = 'REJECTED', + CANCELED = 'CANCELED' +} diff --git a/dinky-web/src/types/AuthCenter/init.d.ts b/dinky-web/src/types/AuthCenter/init.d.ts index 6d0a31b4de..32f2b3f006 100644 --- a/dinky-web/src/types/AuthCenter/init.d.ts +++ b/dinky-web/src/types/AuthCenter/init.d.ts @@ -26,7 +26,8 @@ import { TenantListState, TenantTransferState, TokenListState, - UserListState + UserListState, + ApprovalListState } from '@/types/AuthCenter/state.d'; import { InitContextMenuPosition } from '@/types/Public/state.d'; @@ -144,3 +145,10 @@ export const InitTokenListState: TokenListState = { addedOpen: false, editOpen: false }; + +export const InitApprovalList: ApprovalListState = { + approvalList: [], + loading: false, + addedOpen: false, + editOpen: false +}; diff --git a/dinky-web/src/types/AuthCenter/state.d.ts b/dinky-web/src/types/AuthCenter/state.d.ts index 1cc8981708..e1cc0c4c36 100644 --- a/dinky-web/src/types/AuthCenter/state.d.ts +++ b/dinky-web/src/types/AuthCenter/state.d.ts @@ -17,7 +17,7 @@ * */ -import { RowPermissions, SysMenu, SysToken, UserBaseInfo } from '@/types/AuthCenter/data.d'; +import { RowPermissions, SysMenu, SysToken, UserBaseInfo, ApprovalBasicInfo } from '@/types/AuthCenter/data.d'; import { BaseState, ContextMenuPosition } from '@/types/Public/state.d'; import { Key } from '@ant-design/pro-components'; @@ -112,3 +112,7 @@ export interface UserListState extends BaseState { export interface TokenListState extends BaseState { value: Partial; } + +export interface ApprovalListState extends BaseState { + approvalList: ApprovalBasicInfo[]; +} diff --git a/dinky-web/src/types/Public/constants.tsx b/dinky-web/src/types/Public/constants.tsx index c1292a7cad..7bd866a849 100644 --- a/dinky-web/src/types/Public/constants.tsx +++ b/dinky-web/src/types/Public/constants.tsx @@ -243,6 +243,8 @@ export enum PermissionConstants { SETTING_GLOBAL_METRICS_EDIT = '/settings/globalsetting/metrics/edit', SETTING_GLOBAL_RESOURCE = '/settings/globalsetting/resource', SETTING_GLOBAL_RESOURCE_EDIT = '/settings/globalsetting/resource/edit', + SETTING_GLOBAL_APPROVAL = '/settings/globalsetting/approval', + SETTING_GLOBAL_APPROVAL_EDIT = '/settings/globalsetting/approval/edit', /** * system log