mirror of
https://github.com/semaphoreui/semaphore.git
synced 2024-12-12 03:01:06 +01:00
feat(stats): add chart to ui
This commit is contained in:
parent
830ada9377
commit
5263bb5bcf
@ -2,11 +2,11 @@ package projects
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gorilla/context"
|
||||
"github.com/semaphoreui/semaphore/api/helpers"
|
||||
"github.com/semaphoreui/semaphore/db"
|
||||
"github.com/semaphoreui/semaphore/services/tasks"
|
||||
"github.com/semaphoreui/semaphore/util"
|
||||
"github.com/gorilla/context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -218,3 +218,16 @@ func RemoveTask(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func GetTaskStats(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
stats, err := helpers.Store(r).GetTaskStats(project.ID, nil, db.TaskStatUnitDay, db.TaskFilter{})
|
||||
if err != nil {
|
||||
util.LogErrorWithFields(err, log.Fields{"error": "Bad request. Cannot get task stats from database"})
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
|
@ -317,6 +317,7 @@ func Route() *mux.Router {
|
||||
projectTmplManagement.HandleFunc("/{template_id}/tasks", projects.GetAllTasks).Methods("GET")
|
||||
projectTmplManagement.HandleFunc("/{template_id}/tasks/last", projects.GetLastTasks).Methods("GET")
|
||||
projectTmplManagement.HandleFunc("/{template_id}/schedules", projects.GetTemplateSchedules).Methods("GET")
|
||||
projectTmplManagement.HandleFunc("/{template_id}/stats", projects.GetTaskStats).Methods("GET")
|
||||
|
||||
projectTaskManagement := projectUserAPI.PathPrefix("/tasks").Subrouter()
|
||||
projectTaskManagement.Use(projects.GetTaskMiddleware)
|
||||
|
20
db/Store.go
20
db/Store.go
@ -92,6 +92,24 @@ func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
type TaskStatUnit string
|
||||
|
||||
const TaskStatUnitDay TaskStatUnit = "day"
|
||||
const TaskStatUnitWeek TaskStatUnit = "week"
|
||||
const TaskStatUnitMonth TaskStatUnit = "month"
|
||||
|
||||
type TaskFilter struct {
|
||||
From *time.Time `json:"from"`
|
||||
To *time.Time `json:"to"`
|
||||
UserID *int `json:"user_id"`
|
||||
}
|
||||
|
||||
type TaskStat struct {
|
||||
Date string `json:"date"`
|
||||
CountByStatus map[string]int `json:"count_by_status"`
|
||||
AvgDuration int `json:"avg_duration"`
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
// Connect connects to the database.
|
||||
// Token parameter used if PermanentConnection returns false.
|
||||
@ -271,6 +289,8 @@ type Store interface {
|
||||
GetTemplateVaults(projectID int, templateID int) ([]TemplateVault, error)
|
||||
CreateTemplateVault(vault TemplateVault) (TemplateVault, error)
|
||||
UpdateTemplateVaults(projectID int, templateID int, vaults []TemplateVault) error
|
||||
|
||||
GetTaskStats(projectID int, templateID *int, unit TaskStatUnit, filter TaskFilter) ([]TaskStat, error)
|
||||
}
|
||||
|
||||
var AccessKeyProps = ObjectProps{
|
||||
|
@ -820,6 +820,11 @@ func (d *BoltDb) isObjectInUse(bucketID int, objProps db.ObjectProps, objID obje
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetTaskStats(projectID int, templateID *int, unit db.TaskStatUnit, filter db.TaskFilter) (stats []db.TaskStat, err error) {
|
||||
err = fmt.Errorf("not implmenented")
|
||||
return
|
||||
}
|
||||
|
||||
func CreateTestStore() *BoltDb {
|
||||
util.Config = &util.ConfigType{
|
||||
BoltDb: &util.DbConfig{},
|
||||
|
@ -4,11 +4,6 @@ import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/go-gorp/gorp/v3"
|
||||
_ "github.com/go-sql-driver/mysql" // imports mysql driver
|
||||
@ -16,6 +11,10 @@ import (
|
||||
"github.com/semaphoreui/semaphore/db"
|
||||
"github.com/semaphoreui/semaphore/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SqlDb struct {
|
||||
@ -761,3 +760,58 @@ func (d *SqlDb) GetObjectReferences(objectProps db.ObjectProps, referringObjectP
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetTaskStats(projectID int, templateID *int, unit db.TaskStatUnit, filter db.TaskFilter) (stats []db.TaskStat, err error) {
|
||||
|
||||
if unit != db.TaskStatUnitDay {
|
||||
err = fmt.Errorf("only day unit is supported")
|
||||
return
|
||||
}
|
||||
|
||||
var res []struct {
|
||||
Date string `db:"date"`
|
||||
Status string `db:"status"`
|
||||
Count int `db:"count"`
|
||||
}
|
||||
|
||||
q := squirrel.Select("DATE(created) AS date, status, COUNT(*) AS count").
|
||||
From("task").
|
||||
Where("project_id=?", projectID).
|
||||
GroupBy("DATE(created), status").
|
||||
OrderBy("DATE(created) DESC")
|
||||
|
||||
if templateID != nil {
|
||||
q = q.Where("template_id=?", *templateID)
|
||||
}
|
||||
|
||||
if filter.UserID != nil {
|
||||
q = q.Where("user_id=?", *filter.UserID)
|
||||
}
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = d.selectAll(&res, query, args...)
|
||||
|
||||
var date string
|
||||
var stat *db.TaskStat
|
||||
|
||||
for _, r := range res {
|
||||
|
||||
if date != r.Date {
|
||||
date = r.Date
|
||||
stat = &db.TaskStat{
|
||||
Date: date,
|
||||
CountByStatus: make(map[string]int),
|
||||
}
|
||||
stats = append(stats, *stat)
|
||||
}
|
||||
|
||||
stat.CountByStatus[r.Status] = r.Count
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
738
web/package-lock.json
generated
738
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
108
web/src/components/LineChart.vue
Normal file
108
web/src/components/LineChart.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<BarChartGenerator
|
||||
:chart-id="chartId"
|
||||
:dataset-id-key="chartId"
|
||||
:chart-options="chartOptions"
|
||||
:chart-data="chartData"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
TimeScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
|
||||
import 'chartjs-adapter-moment';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
PointElement,
|
||||
TimeScale,
|
||||
);
|
||||
|
||||
export default {
|
||||
|
||||
name: 'LineChart',
|
||||
|
||||
props: {
|
||||
|
||||
sourceData: Array,
|
||||
|
||||
chartId: String,
|
||||
},
|
||||
|
||||
computed: {
|
||||
chartData() {
|
||||
return {
|
||||
|
||||
labels: (this.sourceData || []).map((row) => new Date(row.date)),
|
||||
|
||||
datasets: [
|
||||
{
|
||||
label: 'Success',
|
||||
borderColor: '#4caf50',
|
||||
backgroundColor: '#4caf50',
|
||||
data: (this.sourceData || []).map((row) => row.count_by_status.success),
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
{
|
||||
label: 'Failed',
|
||||
borderColor: '#ff5252',
|
||||
backgroundColor: '#ff5252',
|
||||
data: (this.sourceData || []).map((row) => row.count_by_status.error),
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
{
|
||||
label: 'Stopped',
|
||||
borderColor: '#555',
|
||||
backgroundColor: '#555',
|
||||
data: (this.sourceData || []).map((row) => row.count_by_status.stopped),
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chartOptions: {
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
@ -2,6 +2,8 @@ import Vue from 'vue';
|
||||
import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
import { AnsiUp } from 'ansi_up';
|
||||
import { Line, Bar } from 'vue-chartjs/legacy';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import vuetify from './plugins/vuetify';
|
||||
@ -82,6 +84,9 @@ Vue.filter('formatMilliseconds', (value) => {
|
||||
return moment.duration(duration, 'milliseconds').humanize();
|
||||
});
|
||||
|
||||
Vue.component('LineChartGenerator', Line);
|
||||
Vue.component('BarChartGenerator', Bar);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
vuetify,
|
||||
|
@ -85,6 +85,9 @@
|
||||
</v-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<LineChart :source-data="stats"/>
|
||||
|
||||
</v-container>
|
||||
|
||||
</template>
|
||||
@ -94,8 +97,11 @@ import {
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_TITLES,
|
||||
} from '@/lib/constants';
|
||||
import axios from 'axios';
|
||||
import LineChart from '@/components/LineChart.vue';
|
||||
|
||||
export default {
|
||||
components: { LineChart },
|
||||
props: {
|
||||
template: Object,
|
||||
repositories: Array,
|
||||
@ -107,7 +113,15 @@ export default {
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_TITLES,
|
||||
TEMPLATE_TYPE_ACTION_TITLES,
|
||||
stats: null,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.stats = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.template.project_id}/templates/${this.template.id}/stats`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user