Skip to content

Commit 7df1820

Browse files
committed
feat(ui): add stats
1 parent 8d89127 commit 7df1820

File tree

9 files changed

+342
-143
lines changed

9 files changed

+342
-143
lines changed

api/projects/tasks.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package projects
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"errors"
67
"github.com/gorilla/context"
78
"github.com/semaphoreui/semaphore/api/helpers"
@@ -128,16 +129,24 @@ func GetTaskStages(w http.ResponseWriter, r *http.Request) {
128129
task := context.Get(r, "task").(db.Task)
129130
project := context.Get(r, "project").(db.Project)
130131

131-
var output []db.TaskOutput
132-
output, err := helpers.Store(r).GetTaskOutputs(project.ID, task.ID, db.RetrieveQueryParams{})
132+
stages, err := helpers.Store(r).GetTaskStages(project.ID, task.ID)
133133

134134
if err != nil {
135-
util.LogErrorF(err, log.Fields{"error": "Bad request. Cannot get task output from database"})
136-
w.WriteHeader(http.StatusBadRequest)
135+
helpers.WriteError(w, err)
137136
return
138137
}
139138

140-
helpers.WriteJSON(w, http.StatusOK, output)
139+
for i := range stages {
140+
var res any
141+
err = json.Unmarshal([]byte(stages[i].JSON), &res)
142+
if err != nil {
143+
helpers.WriteError(w, err)
144+
return
145+
}
146+
stages[i].Result = res
147+
}
148+
149+
helpers.WriteJSON(w, http.StatusOK, stages)
141150
}
142151

143152
// GetTaskOutput returns the logged task output by id and writes it as json or returns error

api/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ func Route() *mux.Router {
382382
projectTaskManagement.HandleFunc("/{task_id}/raw_output", projects.GetTaskRawOutput).Methods("GET", "HEAD")
383383
projectTaskManagement.HandleFunc("/{task_id}", projects.GetTask).Methods("GET", "HEAD")
384384
projectTaskManagement.HandleFunc("/{task_id}", projects.RemoveTask).Methods("DELETE")
385+
projectTaskManagement.HandleFunc("/{task_id}/stages", projects.GetTaskStages).Methods("GET", "HEAD")
385386

386387
projectScheduleManagement := projectUserAPI.PathPrefix("/schedules").Subrouter()
387388
projectScheduleManagement.Use(projects.SchedulesMiddleware)

db/Store.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,7 @@ type Store interface {
346346
EndTaskStage(taskID int, stageID int, end time.Time, endOutputID int) error
347347
CreateTaskStageResult(taskID int, stageID int, result map[string]any) error
348348

349-
GetTaskStages(projectID int, taskID int) ([]TaskStage, error)
350-
GetTaskStagesByType(projectID int, taskID int, stage TaskStageType) ([]TaskStage, error)
349+
GetTaskStages(projectID int, taskID int) ([]TaskStageWithResult, error)
351350
GetTaskStageResult(projectID int, taskID int, stageID int) (TaskStageResult, error)
352351
GetTaskStageOutputs(projectID int, taskID int, stageID int) ([]TaskOutput, error)
353352

db/Task.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,14 +225,21 @@ type TaskStage struct {
225225
Type TaskStageType `db:"type" json:"type"`
226226
}
227227

228+
type TaskStageWithResult struct {
229+
ID int `db:"id" json:"id"`
230+
TaskID int `db:"task_id" json:"task_id"`
231+
Start *time.Time `db:"start" json:"start"`
232+
End *time.Time `db:"end" json:"end"`
233+
StartOutputID *int `db:"start_output_id" json:"start_output_id"`
234+
EndOutputID *int `db:"end_output_id" json:"end_output_id"`
235+
Type TaskStageType `db:"type" json:"type"`
236+
JSON string `db:"json" json:"-"`
237+
Result any `db:"-" json:"result"`
238+
}
239+
228240
type TaskStageResult struct {
229241
ID int `db:"id" json:"id"`
230242
TaskID int `db:"task_id" json:"task_id"`
231243
StageID int `db:"stage_id" json:"stage_id"`
232244
JSON string `db:"json" json:"json"`
233245
}
234-
235-
type TaskStageWithResult struct {
236-
TaskStage
237-
Result map[string]any `db:"result" json:"result"`
238-
}

db/bolt/task.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func (d *BoltDb) CreateTaskStage(stage db.TaskStage) (db.TaskStage, error) {
1414
return newOutput.(db.TaskStage), nil
1515
}
1616

17-
func (d *BoltDb) GetTaskStages(projectID int, taskID int) (res []db.TaskStage, err error) {
17+
func (d *BoltDb) GetTaskStages(projectID int, taskID int) (res []db.TaskStageWithResult, err error) {
1818
// check if task exists in the project
1919
_, err = d.GetTask(projectID, taskID)
2020

@@ -253,10 +253,6 @@ func (d *BoltDb) CreateTaskStageResult(taskID int, stageID int, result map[strin
253253
return nil
254254
}
255255

256-
func (d *BoltDb) GetTaskStagesByType(projectID int, taskID int, stage db.TaskStageType) (res []db.TaskStage, err error) {
257-
return
258-
}
259-
260256
func (d *BoltDb) GetTaskStageResult(projectID int, taskID int, stageID int) (res db.TaskStageResult, err error) {
261257
return
262258
}

db/sql/task.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,15 @@ func (d *SqlDb) GetTaskStageResult(projectID int, taskID int, stageID int) (res
9090
return
9191
}
9292

93-
func (d *SqlDb) getTaskStages(projectID int, taskID int, stageType *db.TaskStageType) (res []db.TaskStage, err error) {
93+
func (d *SqlDb) getTaskStages(projectID int, taskID int, stageType *db.TaskStageType) (res []db.TaskStageWithResult, err error) {
9494
if err = d.validateTask(projectID, taskID); err != nil {
9595
return
9696
}
9797

98-
q := squirrel.Select("*").
99-
From(db.TaskStageProps.TableName).
100-
Where(squirrel.Eq{"task_id": taskID})
98+
q := squirrel.Select("p.*, pu.json").
99+
From("task__stage as p").
100+
Join("task__stage_result as pu on pu.stage_id=p.id").
101+
Where("pu.task_id=?", taskID)
101102

102103
if stageType != nil {
103104
q = q.Where(squirrel.Eq{"type": *stageType})
@@ -114,14 +115,10 @@ func (d *SqlDb) getTaskStages(projectID int, taskID int, stageType *db.TaskStage
114115
return
115116
}
116117

117-
func (d *SqlDb) GetTaskStages(projectID int, taskID int) ([]db.TaskStage, error) {
118+
func (d *SqlDb) GetTaskStages(projectID int, taskID int) ([]db.TaskStageWithResult, error) {
118119
return d.getTaskStages(projectID, taskID, nil)
119120
}
120121

121-
func (d *SqlDb) GetTaskStagesByType(projectID int, taskID int, stageType db.TaskStageType) ([]db.TaskStage, error) {
122-
return d.getTaskStages(projectID, taskID, &stageType)
123-
}
124-
125122
func (d *SqlDb) clearTasks(projectID int, templateID int, maxTasks int) {
126123
tpl, err := d.GetTemplate(projectID, templateID)
127124
if err != nil {

web/src/App.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,25 @@
756756
}
757757
}
758758
759+
.v-input {
760+
.v-input__slot fieldset {
761+
//border-radius: 8px;
762+
//border: 0;
763+
//border-color: rgba(133, 133, 133, 0.3);
764+
//background-color: rgba(133, 133, 133, 0.05);
765+
}
766+
767+
.v-label--active {
768+
//background: white;
769+
//padding-left: 3px;
770+
//padding-right: 3px;
771+
//border-radius: 4px;
772+
//text-shadow: 0px 0px 2px black;
773+
//color: black;
774+
//font-weight: 500;
775+
}
776+
}
777+
759778
@import '~vuetify/src/styles/styles.sass';
760779
@media #{map-get($display-breakpoints, 'xl-only')} {
761780
.CenterToScreen {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
2+
<div style="overflow: hidden;" class="pb-5">
3+
4+
<div class="pl-5 pt-5 d-flex" style="column-gap: 10px;">
5+
<div class="AnsibleServerStatus AnsibleServerStatus--ok">
6+
<div class="AnsibleServerStatus__count">{{ okServers }}</div>
7+
<div>OK SERVERS</div>
8+
</div>
9+
10+
<div class="AnsibleServerStatus AnsibleServerStatus--bad">
11+
<div class="AnsibleServerStatus__count">{{ notOkServers }}</div>
12+
<div>NOT OK SERVERS</div>
13+
</div>
14+
</div>
15+
16+
<v-btn-toggle class="pl-5 mt-8 mb-3" dense v-model="tab" mandatory>
17+
<v-btn value="notOkServers">
18+
Not ok servers
19+
</v-btn>
20+
<v-btn value="allServers">
21+
All servers
22+
</v-btn>
23+
</v-btn-toggle>
24+
25+
<v-simple-table v-if="tab === 'notOkServers'">
26+
<template v-slot:default>
27+
<thead>
28+
<tr>
29+
<th style="width: 150px;">Server</th>
30+
<th style="width: 200px;">Task</th>
31+
<th style="calc(100% - 350px);">Error</th>
32+
</tr>
33+
</thead>
34+
<tbody>
35+
<tr v-for="(task, index) in failedTasks" :key="index">
36+
<td style="width: 150px;">{{ task.host }}</td>
37+
<td style="width: 200px;">{{ task.task }}</td>
38+
<td>
39+
<div style="overflow: hidden; color: red; max-width: 400px; text-overflow: ellipsis">
40+
{{ task.answer }}
41+
</div>
42+
</td>
43+
</tr>
44+
</tbody>
45+
</template>
46+
</v-simple-table>
47+
48+
<v-simple-table v-else-if="tab === 'allServers'">
49+
<template v-slot:default>
50+
<thead>
51+
<tr>
52+
<th>Host</th>
53+
<th>Changed</th>
54+
<th>Failed</th>
55+
<th>Ignored</th>
56+
<th>Ok</th>
57+
<th>Rescued</th>
58+
<th>Skipped</th>
59+
<th>Unreachable</th>
60+
</tr>
61+
</thead>
62+
<tbody>
63+
<tr v-for="(host, index) in hosts" :key="index">
64+
<td>{{ host.host }}</td>
65+
<td>{{ host.changed }}</td>
66+
<td>{{ host.failed }}</td>
67+
<td>{{ host.ignored }}</td>
68+
<td>{{ host.ok }}</td>
69+
<td>{{ host.rescued }}</td>
70+
<td>{{ host.skipped }}</td>
71+
<td>{{ host.unreachable }}</td>
72+
</tr>
73+
</tbody>
74+
</template>
75+
</v-simple-table>
76+
</div>
77+
</template>
78+
<style lang="scss">
79+
.AnsibleServerStatus {
80+
text-align: center;
81+
width: 250px;
82+
font-weight: bold;
83+
color: white;
84+
font-size: 24px;
85+
line-height: 1.2;
86+
border-radius: 8px;
87+
}
88+
89+
.AnsibleServerStatus__count {
90+
font-size: 100px;
91+
}
92+
93+
.AnsibleServerStatus--ok {
94+
background-color: green;
95+
}
96+
97+
.AnsibleServerStatus--bad {
98+
background-color: red;
99+
}
100+
</style>
101+
102+
<script>
103+
104+
export default {
105+
props: {
106+
stages: Array,
107+
},
108+
109+
data() {
110+
return {
111+
okServers: 0,
112+
notOkServers: 0,
113+
tab: 'notOkServers',
114+
};
115+
},
116+
117+
watch: {
118+
stages() {
119+
this.calcStats();
120+
},
121+
},
122+
123+
computed: {
124+
failedTasks() {
125+
const running = (this.stages || [])
126+
.filter((stage) => stage.type === 'running')[0];
127+
return running?.result.failed || {};
128+
},
129+
hosts() {
130+
const running = (this.stages || [])
131+
.filter((stage) => stage.type === 'print_result')[0];
132+
return running?.result.hosts || [];
133+
},
134+
},
135+
136+
created() {
137+
this.calcStats();
138+
},
139+
140+
methods: {
141+
calcStats() {
142+
this.hosts.forEach((host) => {
143+
if (host.failed > 0 || host.unreachable > 0) {
144+
this.notOkServers += 1;
145+
} else {
146+
this.okServers += 1;
147+
}
148+
});
149+
},
150+
},
151+
};
152+
</script>

0 commit comments

Comments
 (0)