From 5987f005237039a85e6e94d5439680eee1d82b4a Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 1 May 2023 23:14:20 +0900
Subject: [PATCH] Add rerun workflow button and refactor to use SVG octicons
 (#24350)

Changes:
- Add rerun workflow button. Then users can rerun the whole workflow by
only one-click.
- Refactor to use SVG octicons in RepoActionView.vue

![image](https://user-images.githubusercontent.com/18380374/234736083-dea9b333-ec11-4095-a113-763f3716fba7.png)

![image](https://user-images.githubusercontent.com/18380374/234736107-d657d19c-f70a-42f4-985f-156a8c7efb7a.png)

![image](https://user-images.githubusercontent.com/18380374/234736160-9ad372df-7089-4d18-9bab-48bca3f01878.png)

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/web/repo/actions/view.go         | 41 ++++++++++++++++++++----
 routers/web/web.go                       |  3 +-
 web_src/js/components/RepoActionView.vue | 33 +++++++++----------
 web_src/js/svg.js                        |  2 ++
 4 files changed, 54 insertions(+), 25 deletions(-)

diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index c553aef9ae..f96cd2acf8 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -55,6 +55,7 @@ type ViewResponse struct {
 			Status     string     `json:"status"`
 			CanCancel  bool       `json:"canCancel"`
 			CanApprove bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
+			CanRerun   bool       `json:"canRerun"`
 			Done       bool       `json:"done"`
 			Jobs       []*ViewJob `json:"jobs"`
 			Commit     ViewCommit `json:"commit"`
@@ -136,6 +137,7 @@ func ViewPost(ctx *context_module.Context) {
 	resp.State.Run.Link = run.Link()
 	resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
+	resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.Done = run.Status.IsDone()
 	resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
 	resp.State.Run.Status = run.Status.String()
@@ -238,7 +240,7 @@ func ViewPost(ctx *context_module.Context) {
 	ctx.JSON(http.StatusOK, resp)
 }
 
-func Rerun(ctx *context_module.Context) {
+func RerunOne(ctx *context_module.Context) {
 	runIndex := ctx.ParamsInt64("run")
 	jobIndex := ctx.ParamsInt64("job")
 
@@ -246,10 +248,37 @@ func Rerun(ctx *context_module.Context) {
 	if ctx.Written() {
 		return
 	}
+
+	if err := rerunJob(ctx, job); err != nil {
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	ctx.JSON(http.StatusOK, struct{}{})
+}
+
+func RerunAll(ctx *context_module.Context) {
+	runIndex := ctx.ParamsInt64("run")
+
+	_, jobs := getRunJobs(ctx, runIndex, 0)
+	if ctx.Written() {
+		return
+	}
+
+	for _, j := range jobs {
+		if err := rerunJob(ctx, j); err != nil {
+			ctx.Error(http.StatusInternalServerError, err.Error())
+			return
+		}
+	}
+
+	ctx.JSON(http.StatusOK, struct{}{})
+}
+
+func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
 	status := job.Status
 	if !status.IsDone() {
-		ctx.JSON(http.StatusOK, struct{}{})
-		return
+		return nil
 	}
 
 	job.TaskID = 0
@@ -261,13 +290,11 @@ func Rerun(ctx *context_module.Context) {
 		_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
 		return err
 	}); err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
-		return
+		return err
 	}
 
 	actions_service.CreateCommitStatus(ctx, job)
-
-	ctx.JSON(http.StatusOK, struct{}{})
+	return nil
 }
 
 func Cancel(ctx *context_module.Context) {
diff --git a/routers/web/web.go b/routers/web/web.go
index e63add51f3..5357c55500 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1186,10 +1186,11 @@ func registerRoutes(m *web.Route) {
 					m.Combo("").
 						Get(actions.View).
 						Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
-					m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
+					m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne)
 				})
 				m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
 				m.Post("/approve", reqRepoActionsWriter, actions.Approve)
+				m.Post("/rerun", reqRepoActionsWriter, actions.RerunAll)
 			})
 		}, reqRepoActionsReader, actions.MustEnableActions)
 
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 6fcd6ce880..00411654a3 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -6,11 +6,14 @@
         <div class="action-title">
           {{ run.title }}
         </div>
-        <button class="run_approve" @click="approveRun()" v-if="run.canApprove">
-          <i class="play circle outline icon"/>
+        <button class="action-control-button text green" @click="approveRun()" v-if="run.canApprove">
+          <SvgIcon name="octicon-play" :size="20"/>
         </button>
-        <button class="run_cancel" @click="cancelRun()" v-else-if="run.canCancel">
-          <i class="stop circle outline icon"/>
+        <button class="action-control-button text red" @click="cancelRun()" v-else-if="run.canCancel">
+          <SvgIcon name="octicon-x-circle-fill" :size="20"/>
+        </button>
+        <button class="action-control-button text green" @click="rerun()" v-else-if="run.canRerun">
+          <SvgIcon name="octicon-sync" :size="20"/>
         </button>
       </div>
       <div class="action-commit-summary">
@@ -106,6 +109,7 @@ const sfc = {
         status: '',
         canCancel: false,
         canApprove: false,
+        canRerun: false,
         done: false,
         jobs: [
           // {
@@ -193,6 +197,11 @@ const sfc = {
       await this.fetchPost(`${jobLink}/rerun`);
       window.location.href = jobLink;
     },
+    // rerun workflow
+    async rerun() {
+      await this.fetchPost(`${this.run.link}/rerun`);
+      window.location.href = this.run.link;
+    },
     // cancel a run
     cancelRun() {
       this.fetchPost(`${this.run.link}/cancel`);
@@ -366,26 +375,16 @@ export function ansiLogToHTML(line) {
   margin: 0 20px 20px 20px;
 }
 
-.action-view-header .run_cancel {
+.action-view-header .action-control-button {
   border: none;
-  color: var(--color-red);
   background-color: transparent;
   outline: none;
   cursor: pointer;
   transition: transform 0.2s;
+  display: flex;
 }
 
-.action-view-header .run_approve {
-  border: none;
-  color: var(--color-green);
-  background-color: transparent;
-  outline: none;
-  cursor: pointer;
-  transition: transform 0.2s;
-}
-
-.action-view-header .run_cancel:hover,
-.action-view-header .run_approve:hover {
+.action-view-header .action-control-button:hover {
   transform: scale(130%);
 }
 
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 821ed9fd43..024b1fbc16 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -18,6 +18,7 @@ import octiconLink from '../../public/img/svg/octicon-link.svg';
 import octiconLock from '../../public/img/svg/octicon-lock.svg';
 import octiconMilestone from '../../public/img/svg/octicon-milestone.svg';
 import octiconMirror from '../../public/img/svg/octicon-mirror.svg';
+import octiconPlay from '../../public/img/svg/octicon-play.svg';
 import octiconProject from '../../public/img/svg/octicon-project.svg';
 import octiconRepo from '../../public/img/svg/octicon-repo.svg';
 import octiconRepoForked from '../../public/img/svg/octicon-repo-forked.svg';
@@ -79,6 +80,7 @@ const svgs = {
   'octicon-milestone': octiconMilestone,
   'octicon-mirror': octiconMirror,
   'octicon-organization': octiconOrganization,
+  'octicon-play': octiconPlay,
   'octicon-plus': octiconPlus,
   'octicon-project': octiconProject,
   'octicon-repo': octiconRepo,