Added i18n, started working on monitoring

This commit is contained in:
simon987 2019-02-05 20:11:52 -05:00
parent 22f4a6b358
commit 87f4d08984
22 changed files with 230 additions and 785 deletions

View File

@ -65,8 +65,7 @@ func New() *WebAPI {
api.router.POST("/project/create", LogRequestMiddleware(api.ProjectCreate))
api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet))
api.router.POST("/project/update/:id", LogRequestMiddleware(api.ProjectUpdate))
api.router.GET("/project/stats/:id", LogRequestMiddleware(api.ProjectGetStats))
api.router.GET("/project/stats", LogRequestMiddleware(api.ProjectGetAllStats))
api.router.GET("/project/list", LogRequestMiddleware(api.ProjectGetAllProjects))
api.router.POST("/task/create", LogRequestMiddleware(api.TaskCreate))
api.router.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject))

View File

@ -43,16 +43,10 @@ type GetProjectResponse struct {
Project *storage.Project `json:"project,omitempty"`
}
type GetProjectStatsResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
Stats *storage.ProjectStats `json:"stats,omitempty"`
}
type GetAllProjectsStatsResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
Stats *[]storage.ProjectStats `json:"stats,omitempty"`
type GetAllProjectsResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
Projects *[]storage.Project `json:"projects,omitempty"`
}
func (api *WebAPI) ProjectCreate(r *Request) {
@ -200,38 +194,12 @@ func (api *WebAPI) ProjectGet(r *Request) {
}
}
func (api *WebAPI) ProjectGetStats(r *Request) {
func (api *WebAPI) ProjectGetAllProjects(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
if err != nil {
r.Json(GetProjectStatsResponse{
Ok: false,
Message: "Could not parse request",
}, 400)
return
}
projects := api.Database.GetAllProjects()
stats := api.Database.GetProjectStats(id)
if stats != nil && stats.Project != nil {
r.OkJson(GetProjectStatsResponse{
Ok: true,
Stats: stats,
})
} else {
r.Json(GetProjectStatsResponse{
Ok: false,
Message: "Project not found",
}, 404)
}
}
func (api *WebAPI) ProjectGetAllStats(r *Request) {
stats := api.Database.GetAllProjectsStats()
r.OkJson(GetAllProjectsStatsResponse{
Ok: true,
Stats: stats,
r.OkJson(GetAllProjectsResponse{
Ok: true,
Projects: projects,
})
}

View File

@ -1,5 +1,6 @@
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry,
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring, worker_verifies_task;
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot,
worker_verifies_task;
DROP TYPE IF EXISTS status;
DROP TYPE IF EXISTS log_level;
@ -87,12 +88,15 @@ CREATE TABLE manager_has_role_on_project
project INTEGER REFERENCES project (id)
);
CREATE TABLE project_monitoring
CREATE TABLE project_monitoring_snapshot
(
project INT REFERENCES project (id),
new_task_count INT,
failed_task_count INT,
closed_task_count INT
project INT REFERENCES project (id),
new_task_count INT,
failed_task_count INT,
closed_task_count INT,
awaiting_verification_task_count INT,
worker_access_count INT,
timestamp INT
);
CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS

44
storage/monitoring.go Normal file
View File

@ -0,0 +1,44 @@
package storage
type ProjectMonitoringSnapshot struct {
NewTaskCount int64
FailedTaskCount int64
ClosedTaskCount int64
WorkerAccessCount int64
TimeStamp int64
}
func (database *Database) MakeProjectSnapshots() {
db := database.getDB()
_, err := db.Exec(`
INSERT INTO project_monitoring_snapshot
(project, new_task_count, failed_task_count, closed_task_count, worker_access_count, timestamp)
SELECT id,
(SELECT COUNT(*) FROM task WHERE task.project = project.id AND status = 1),
(SELECT COUNT(*) FROM task WHERE task.project = project.id AND status = 2),
closed_task_count,
(SELECT COUNT(*) FROM worker_has_access_to_project wa WHERE wa.project = project.id),
extract(epoch from now() at time zone 'utc')
FROM project`)
handleErr(err)
}
func (database *Database) GetMonitoringSnapshots(pid int64, from int64, to int64) (ss *[]ProjectMonitoringSnapshot) {
db := database.getDB()
rows, err := db.Query(`SELECT new_task_count, failed_task_count, closed_task_count,
worker_access_count, timestamp FROM project_monitoring_snapshot
WHERE project=$1 AND timestamp BETWEEN $2 AND $3`, pid, from, to)
handleErr(err)
for rows.Next() {
s := ProjectMonitoringSnapshot{}
err := rows.Scan(&s.NewTaskCount, &s.FailedTaskCount, &s.ClosedTaskCount, &s.WorkerAccessCount, &s.TimeStamp)
handleErr(err)
}
return nil
}

View File

@ -22,14 +22,6 @@ type AssignedTasks struct {
TaskCount int64 `json:"task_count"`
}
type ProjectStats struct {
Project *Project `json:"project"`
NewTaskCount int64 `json:"new_task_count"`
FailedTaskCount int64 `json:"failed_task_count"`
ClosedTaskCount int64 `json:"closed_task_count"`
Assignees []*AssignedTasks `json:"assignees"`
}
func (database *Database) SaveProject(project *Project) (int64, error) {
db := database.getDB()
id, projectErr := saveProject(project, db)
@ -139,83 +131,28 @@ func (database *Database) UpdateProject(project *Project) error {
return nil
}
func (database *Database) GetProjectStats(id int64) *ProjectStats {
db := database.getDB()
stats := ProjectStats{}
stats.Project = getProject(id, db)
if stats.Project != nil {
row := db.QueryRow(`SELECT
SUM(CASE WHEN status=1 THEN 1 ELSE 0 END) newCount,
SUM(CASE WHEN status=2 THEN 1 ELSE 0 END) failedCount,
SUM(CASE WHEN status=3 THEN 1 ELSE 0 END) closedCount
FROM task WHERE project=$1 GROUP BY project`, id)
err := row.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"id": id,
}).Trace("Get project stats: No task for this project")
}
rows, err := db.Query(`SELECT worker.alias, COUNT(*) as wc FROM TASK
LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1
GROUP BY worker.id ORDER BY wc LIMIT 10`, id)
stats.Assignees = []*AssignedTasks{}
for rows.Next() {
assignee := AssignedTasks{}
var assigneeAlias sql.NullString
err = rows.Scan(&assigneeAlias, &assignee.TaskCount)
handleErr(err)
if assigneeAlias.Valid {
assignee.Assignee = assigneeAlias.String
} else {
assignee.Assignee = "unassigned"
}
stats.Assignees = append(stats.Assignees, &assignee)
}
}
return &stats
}
func (database Database) GetAllProjectsStats() *[]ProjectStats {
var statsList []ProjectStats
func (database Database) GetAllProjects() *[]Project {
var projects []Project
db := database.getDB()
rows, err := db.Query(`SELECT
SUM(CASE WHEN status= 1 THEN 1 ELSE 0 END) newCount,
SUM(CASE WHEN status=2 THEN 1 ELSE 0 END) failedCount,
SUM(CASE WHEN status=3 THEN 1 ELSE 0 END) closedCount,
p.Id, p.priority, p.name, p.clone_url, p.git_repo, p.version, p.motd,
p.public
FROM task RIGHT JOIN project p on task.project = p.id
GROUP BY p.id ORDER BY p.name`)
Id, priority, name, clone_url, git_repo, version, motd, public
FROM project
ORDER BY name`)
handleErr(err)
for rows.Next() {
stats := ProjectStats{}
p := &Project{}
err := rows.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount,
&p.Id, &p.Priority, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version, &p.Motd, &p.Public)
p := Project{}
err := rows.Scan(&p.Id, &p.Priority, &p.Name, &p.CloneUrl,
&p.GitRepo, &p.Version, &p.Motd, &p.Public)
handleErr(err)
stats.Project = p
statsList = append(statsList, stats)
projects = append(projects, p)
}
logrus.WithFields(logrus.Fields{
"statsList": statsList,
"projects": projects,
}).Trace("Get all projects stats")
return &statsList
return &projects
}

View File

@ -122,84 +122,6 @@ func TestGetProjectNotFound(t *testing.T) {
}
}
func TestGetProjectStats(t *testing.T) {
r := createProject(api.CreateProjectRequest{
Motd: "motd",
Name: "Name",
Version: "version",
CloneUrl: "http://github.com/drone/test",
GitRepo: "drone/test",
Priority: 3,
Public: true,
})
pid := r.Id
worker := genWid()
createTask(api.CreateTaskRequest{
Priority: 1,
Project: pid,
MaxRetries: 0,
Recipe: "{}",
}, worker)
createTask(api.CreateTaskRequest{
Priority: 2,
Project: pid,
MaxRetries: 0,
Recipe: "{}",
}, worker)
createTask(api.CreateTaskRequest{
Priority: 3,
Project: pid,
MaxRetries: 0,
Recipe: "{}",
}, worker)
stats := getProjectStats(pid)
if stats.Ok != true {
t.Error()
}
if stats.Stats.Project.Id != pid {
t.Error()
}
if stats.Stats.NewTaskCount != 3 {
t.Error()
}
if stats.Stats.Assignees[0].Assignee != "unassigned" {
t.Error()
}
if stats.Stats.Assignees[0].TaskCount != 3 {
t.Error()
}
}
func TestGetProjectStatsNotFound(t *testing.T) {
r := createProject(api.CreateProjectRequest{
Motd: "eeeeeeeeej",
Name: "Namaaaaaaaaaaaa",
Version: "versionsssssssss",
CloneUrl: "http://github.com/drone/test1",
GitRepo: "drone/test1",
Priority: 1,
})
s := getProjectStats(r.Id)
if s.Ok != true {
t.Error()
}
if s.Stats == nil {
t.Error()
}
}
func TestUpdateProjectValid(t *testing.T) {
pid := createProject(api.CreateProjectRequest{
@ -337,18 +259,6 @@ func getProject(id int64) (*api.GetProjectResponse, *http.Response) {
return &getResp, r
}
func getProjectStats(id int64) *api.GetProjectStatsResponse {
r := Get(fmt.Sprintf("/project/stats/%d", id), nil)
var getResp api.GetProjectStatsResponse
data, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(data, &getResp)
handleErr(err)
return &getResp
}
func updateProject(request api.UpdateProjectRequest, pid int64) *api.UpdateProjectResponse {
r := Post(fmt.Sprintf("/project/update/%d", pid), request, nil)

View File

@ -1,5 +1,6 @@
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry,
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring, worker_verifies_task;
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot,
worker_verifies_task;
DROP TYPE IF EXISTS status;
DROP TYPE IF EXISTS log_level;
@ -87,12 +88,15 @@ CREATE TABLE manager_has_role_on_project
project INTEGER REFERENCES project (id)
);
CREATE TABLE project_monitoring
CREATE TABLE project_monitoring_snapshot
(
project INT REFERENCES project (id),
new_task_count INT,
failed_task_count INT,
closed_task_count INT
project INT REFERENCES project (id),
new_task_count INT,
failed_task_count INT,
closed_task_count INT,
awaiting_verification_task_count INT,
worker_access_count INT,
timestamp INT
);
CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS

View File

@ -787,6 +787,22 @@
}
}
},
"@ngx-translate/core": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-11.0.1.tgz",
"integrity": "sha512-nBCa1ZD9fAUY/3eskP3Lql2fNg8OMrYIej1/5GRsfcutx9tG/5fZLCv9m6UCw1aS+u4uK/vXjv1ctG/FdMvaWg==",
"requires": {
"tslib": "^1.9.0"
}
},
"@ngx-translate/http-loader": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-4.0.0.tgz",
"integrity": "sha512-x8LumqydWD7eX9yQTAVeoCM9gFUIGVTUjZqbxdAUavAA3qVnk9wCQux7iHLPXpydl8vyQmLoPQR+fFU+DUDOMA==",
"requires": {
"tslib": "^1.9.0"
}
},
"@schematics/angular": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-7.2.2.tgz",
@ -2099,6 +2115,39 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"chart.js": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.3.tgz",
"integrity": "sha512-3+7k/DbR92m6BsMUYP6M0dMsMVZpMnwkUyNSAbqolHKsbIzH2Q4LWVEHHYq7v0fmEV8whXE0DrjANulw9j2K5g==",
"requires": {
"chartjs-color": "^2.1.0",
"moment": "^2.10.2"
}
},
"chartjs-color": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz",
"integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=",
"requires": {
"chartjs-color-string": "^0.5.0",
"color-convert": "^0.5.3"
},
"dependencies": {
"color-convert": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
"integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0="
}
}
},
"chartjs-color-string": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz",
"integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==",
"requires": {
"color-name": "^1.0.0"
}
},
"chokidar": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
@ -2303,8 +2352,7 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"colors": {
"version": "1.1.2",
@ -2333,7 +2381,8 @@
"commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg=="
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
"dev": true
},
"commondir": {
"version": "1.0.1",
@ -2695,270 +2744,6 @@
"integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
"dev": true
},
"d3": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-5.7.0.tgz",
"integrity": "sha512-8KEIfx+dFm8PlbJN9PI0suazrZ41QcaAufsKE9PRcqYPWLngHIyWJZX96n6IQKePGgeSu0l7rtlueSSNq8Zc3g==",
"requires": {
"d3-array": "1",
"d3-axis": "1",
"d3-brush": "1",
"d3-chord": "1",
"d3-collection": "1",
"d3-color": "1",
"d3-contour": "1",
"d3-dispatch": "1",
"d3-drag": "1",
"d3-dsv": "1",
"d3-ease": "1",
"d3-fetch": "1",
"d3-force": "1",
"d3-format": "1",
"d3-geo": "1",
"d3-hierarchy": "1",
"d3-interpolate": "1",
"d3-path": "1",
"d3-polygon": "1",
"d3-quadtree": "1",
"d3-random": "1",
"d3-scale": "2",
"d3-scale-chromatic": "1",
"d3-selection": "1",
"d3-shape": "1",
"d3-time": "1",
"d3-time-format": "2",
"d3-timer": "1",
"d3-transition": "1",
"d3-voronoi": "1",
"d3-zoom": "1"
}
},
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
},
"d3-axis": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
},
"d3-brush": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz",
"integrity": "sha512-lGSiF5SoSqO5/mYGD5FAeGKKS62JdA1EV7HPrU2b5rTX4qEJJtpjaGLJngjnkewQy7UnGstnFd3168wpf5z76w==",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"d3-chord": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
"integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
"requires": {
"d3-array": "1",
"d3-path": "1"
}
},
"d3-collection": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
},
"d3-color": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz",
"integrity": "sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw=="
},
"d3-contour": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
"integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
"requires": {
"d3-array": "^1.1.1"
}
},
"d3-dispatch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz",
"integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g=="
},
"d3-drag": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz",
"integrity": "sha512-8S3HWCAg+ilzjJsNtWW1Mutl74Nmzhb9yU6igspilaJzeZVFktmY6oO9xOh5TDk+BM2KrNFjttZNoJJmDnkjkg==",
"requires": {
"d3-dispatch": "1",
"d3-selection": "1"
}
},
"d3-dsv": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.0.10.tgz",
"integrity": "sha512-vqklfpxmtO2ZER3fq/B33R/BIz3A1PV0FaZRuFM8w6jLo7sUX1BZDh73fPlr0s327rzq4H6EN1q9U+eCBCSN8g==",
"requires": {
"commander": "2",
"iconv-lite": "0.4",
"rw": "1"
}
},
"d3-ease": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz",
"integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ=="
},
"d3-fetch": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz",
"integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==",
"requires": {
"d3-dsv": "1"
}
},
"d3-force": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.1.2.tgz",
"integrity": "sha512-p1vcHAUF1qH7yR+e8ip7Bs61AHjLeKkIn8Z2gzwU2lwEf2wkSpWdjXG0axudTHsVFnYGlMkFaEsVy2l8tAg1Gw==",
"requires": {
"d3-collection": "1",
"d3-dispatch": "1",
"d3-quadtree": "1",
"d3-timer": "1"
}
},
"d3-format": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz",
"integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ=="
},
"d3-geo": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.3.tgz",
"integrity": "sha512-n30yN9qSKREvV2fxcrhmHUdXP9TNH7ZZj3C/qnaoU0cVf/Ea85+yT7HY7i8ySPwkwjCNYtmKqQFTvLFngfkItQ==",
"requires": {
"d3-array": "1"
}
},
"d3-hierarchy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz",
"integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w=="
},
"d3-interpolate": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
"integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
"requires": {
"d3-color": "1"
}
},
"d3-path": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz",
"integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA=="
},
"d3-polygon": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz",
"integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w=="
},
"d3-quadtree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.5.tgz",
"integrity": "sha512-U2tjwDFbZ75JRAg8A+cqMvqPg1G3BE7UTJn3h8DHjY/pnsAfWdbJKgyfcy7zKjqGtLAmI0q8aDSeG1TVIKRaHQ=="
},
"d3-random": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
"integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
},
"d3-scale": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.1.2.tgz",
"integrity": "sha512-bESpd64ylaKzCDzvULcmHKZTlzA/6DGSVwx7QSDj/EnX9cpSevsdiwdHFYI9ouo9tNBbV3v5xztHS2uFeOzh8Q==",
"requires": {
"d3-array": "^1.2.0",
"d3-collection": "1",
"d3-format": "1",
"d3-interpolate": "1",
"d3-time": "1",
"d3-time-format": "2"
}
},
"d3-scale-chromatic": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz",
"integrity": "sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw==",
"requires": {
"d3-color": "1",
"d3-interpolate": "1"
}
},
"d3-selection": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.2.tgz",
"integrity": "sha512-OoXdv1nZ7h2aKMVg3kaUFbLLK5jXUFAMLD/Tu5JA96mjf8f2a9ZUESGY+C36t8R1WFeWk/e55hy54Ml2I62CRQ=="
},
"d3-shape": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.2.tgz",
"integrity": "sha512-hUGEozlKecFZ2bOSNt7ENex+4Tk9uc/m0TtTEHBvitCBxUNjhzm5hS2GrrVRD/ae4IylSmxGeqX5tWC2rASMlQ==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz",
"integrity": "sha512-hF+NTLCaJHF/JqHN5hE8HVGAXPStEq6/omumPE/SxyHVrR7/qQxusFDo0t0c/44+sCGHthC7yNGFZIEgju0P8g=="
},
"d3-time-format": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz",
"integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==",
"requires": {
"d3-time": "1"
}
},
"d3-timer": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz",
"integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg=="
},
"d3-transition": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.3.tgz",
"integrity": "sha512-tEvo3qOXL6pZ1EzcXxFcPNxC/Ygivu5NoBY6mbzidATAeML86da+JfVIUzon3dNM6UX6zjDx+xbYDmMVtTSjuA==",
"requires": {
"d3-color": "1",
"d3-dispatch": "1",
"d3-ease": "1",
"d3-interpolate": "1",
"d3-selection": "^1.1.0",
"d3-timer": "1"
}
},
"d3-voronoi": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
},
"d3-zoom": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.7.3.tgz",
"integrity": "sha512-xEBSwFx5Z9T3/VrwDkMt+mr0HCzv7XjpGURJ8lWmIC8wxe32L39eWHIasEe/e7Ox8MPU4p1hvH8PKN2olLzIBg==",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -5235,6 +5020,7 @@
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@ -8817,11 +8603,6 @@
"aproba": "^1.1.1"
}
},
"rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
},
"rxjs": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
@ -8848,7 +8629,8 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"sass-graph": {
"version": "2.2.4",

View File

@ -21,8 +21,10 @@
"@angular/platform-browser": "~7.2.0",
"@angular/platform-browser-dynamic": "~7.2.0",
"@angular/router": "~7.2.0",
"@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0",
"chart.js": "^2.7.3",
"core-js": "^2.5.4",
"d3": "^5.7.0",
"lodash": "^4.17.11",
"moment": "^2.23.0",
"rxjs": "~6.3.3",
@ -34,9 +36,9 @@
"@angular/cli": "~7.2.2",
"@angular/compiler-cli": "~7.2.0",
"@angular/language-service": "~7.2.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",

View File

@ -16,12 +16,8 @@ export class ApiService {
return this.http.post(this.url + "/logs", "{\"level\":\"info\", \"since\":10000}");
}
getProjectStats(id: number) {
return this.http.get(this.url + "/project/stats/" + id)
}
getProjects() {
return this.http.get(this.url + "/project/stats")
return this.http.get(this.url + "/project/list")
}
getProject(id: number) {

View File

@ -0,0 +1,3 @@
.nav-spacer {
flex: 1 1 auto;
}

View File

@ -1,11 +1,23 @@
<!--<mat-toolbar>-->
<mat-toolbar color="primary">
<span>{{ "nav.title" | translate }}</span>
<span class="nav-spacer"></span>
<mat-icon class="example-icon">favorite</mat-icon>
<mat-icon class="example-icon">delete</mat-icon>
<mat-form-field [floatLabel]="'never'">
<mat-select [placeholder]="'nav.langSelect' | translate" (selectionChange)="langChange($event)">
<mat-option *ngFor="let lang of langList" [value]="lang.lang">
{{lang.display}}
</mat-option>
</mat-select>
</mat-form-field>
</mat-toolbar>
<ul>
<li><a [routerLink]="''">Index</a></li>
<li><a [routerLink]="'log'">Logs</a></li>
<li><a [routerLink]="'projects'">list</a></li>
<li><a [routerLink]="'new_project'">new project</a></li>
</ul>
<!--</mat-toolbar>-->
<messenger-snack-bar></messenger-snack-bar>

View File

@ -1,4 +1,6 @@
import {Component} from '@angular/core';
import {TranslateService} from "@ngx-translate/core";
import {MatSelectChange} from "@angular/material";
@Component({
selector: 'app-root',
@ -6,5 +8,24 @@ import {Component} from '@angular/core';
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'angular';
langChange(event: MatSelectChange) {
this.translate.use(event.value)
}
langList: any[] = [
{lang: "fr", display: "Français"},
{lang: "en", display: "English"},
];
constructor(private translate: TranslateService) {
translate.addLangs([
"en",
"fr"
]);
translate.setDefaultLang("en");
}
}

View File

@ -18,6 +18,7 @@ import {
MatInputModule,
MatMenuModule,
MatPaginatorModule,
MatSelectModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
@ -28,13 +29,21 @@ import {
} from "@angular/material";
import {ApiService} from "./api.service";
import {MessengerService} from "./messenger.service";
import {HttpClientModule} from "@angular/common/http";
import {HttpClient, HttpClientModule} from "@angular/common/http";
import {ProjectDashboardComponent} from './project-dashboard/project-dashboard.component';
import {ProjectListComponent} from './project-list/project-list.component';
import {CreateProjectComponent} from './create-project/create-project.component';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {UpdateProjectComponent} from './update-project/update-project.component';
import {SnackBarComponent} from "./messenger/snack-bar.component";
import {TranslateLoader, TranslateModule} from "@ngx-translate/core";
import {TranslateHttpLoader} from "@ngx-translate/http-loader";
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
@NgModule({
declarations: [
@ -71,6 +80,15 @@ import {SnackBarComponent} from "./messenger/snack-bar.component";
MatCheckboxModule,
MatDividerModule,
MatSnackBarModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
}
}
),
MatSelectModule
],
exports: [],

View File

@ -12,7 +12,7 @@ import {Router} from "@angular/router";
})
export class CreateProjectComponent implements OnInit {
private project = new Project();
project = new Project();
constructor(private apiService: ApiService,
private messengerService: MessengerService,

View File

@ -12,9 +12,9 @@ import {MatPaginator, MatSort, MatTableDataSource} from "@angular/material";
})
export class LogsComponent implements OnInit {
private logs: LogEntry[] = [];
private data: MatTableDataSource<LogEntry>;
private logsCols: string[] = ["level", "timestamp", "message", "data"];
logs: LogEntry[] = [];
data: MatTableDataSource<LogEntry>;
logsCols: string[] = ["level", "timestamp", "message", "data"];
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;

View File

@ -1,10 +1,5 @@
import {Component, OnInit} from '@angular/core';
import * as d3 from "d3"
import * as _ from "lodash"
import {interval} from "rxjs";
import {ApiService} from "../api.service";
import {ActivatedRoute} from "@angular/router";
@Component({
selector: 'app-project-dashboard',
@ -16,269 +11,6 @@ export class ProjectDashboardComponent implements OnInit {
private projectId;
projectStats;
private pieWidth = 360;
private pieHeight = 360;
private pieRadius = Math.min(this.pieWidth, this.pieHeight) / 2;
private pieArc = d3.arc()
.innerRadius(this.pieRadius / 2)
.outerRadius(this.pieRadius);
private pieFun = d3.pie().value((d) => d.count);
private statusColor = d3.scaleOrdinal().range(['#31a6a2', '#8c2627', '#62f24b']);
private assigneesColor = d3.scaleOrdinal().range(["", "#AAAAAA"].concat(d3.schemePaired));
private statusData: any[];
private assigneesData: any[];
private newTaskCounts: any[] = [];
private failedTaskCounts: any[] = [];
private closedTaskCounts: any[] = [];
private newTaskPath: any;
private failedTaskPath: any;
private closedTaskPath: any;
private maxY: number = 10;
private yAxis: any;
private yScale: any;
private xScale: any;
private range: number;
private line: any;
private statusPath: any;
private statusSvg: any;
private assigneesPath: any;
private assigneesSvg: any;
constructor(private apiService: ApiService, private route: ActivatedRoute) {
}
setupStatusPieChart() {
let tooltip = d3.select("#stooltip");
this.statusSvg = d3.select("#status")
.append('svg')
.attr('width', this.pieWidth)
.attr('height', this.pieHeight)
.append("g")
.attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")");
this.statusPath = this.statusSvg.selectAll()
.data(this.pieFun(this.statusData))
.enter()
.append('path')
.attr('d', this.pieArc)
.attr('fill', (d) => this.statusColor(d.data.label));
this.setupToolTip(this.statusPath, tooltip)
}
setupAssigneesPieChart() {
let tooltip = d3.select("#atooltip");
this.assigneesSvg = d3.select("#assignees")
.append('svg')
.attr('width', this.pieWidth)
.attr('height', this.pieHeight)
.append("g")
.attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")");
this.assigneesPath = this.assigneesSvg.selectAll()
.data(this.pieFun(this.assigneesData))
.enter()
.append('path')
.attr('d', this.pieArc)
.attr('fill', (d) => this.assigneesColor(d.data.label));
this.setupToolTip(this.assigneesPath, tooltip)
}
setupToolTip(x, tooltip) {
x.on('mouseover', (d) => {
let total = d3.sum(this.assigneesData.map((d) => d.count));
let percent = Math.round(1000 * d.data.count / total) / 10;
tooltip.select('.label').html(d.data.label);
tooltip.select('.count').html(d.data.count);
tooltip.select('.percent').html(percent + '%');
tooltip.style('display', 'block');
});
x.on('mouseout', function () {
tooltip.style('display', 'none');
})
}
setupLine() {
let margin = {top: 50, right: 50, bottom: 50, left: 50};
this.range = 600;
let width = 750;
let height = 250;
this.xScale = d3.scaleLinear()
.domain([this.range, 0])
.range([width, 0]);
this.yScale = d3.scaleLinear()
.domain([0, this.maxY])
.range([height, 0]);
this.line = d3.line()
.x((d, i) => this.xScale(i))
.y((d) => this.yScale(d.y))
.curve(d3.curveMonotoneX);
let svg = d3.select("#line").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
this.newTaskPath = svg
.append("path")
.attr("clip-path", "url(#clip)")
.datum(this.newTaskCounts)
.attr("class", "line-new")
.attr("d", this.line);
this.failedTaskPath = svg
.append("path")
.attr("clip-path", "url(#clip)")
.datum(this.failedTaskCounts)
.attr("class", "line-failed")
.attr("d", this.line);
this.closedTaskPath = svg
.append("path")
.attr("clip-path", "url(#clip)")
.datum(this.closedTaskCounts)
.attr("class", "line-closed")
.attr("d", this.line);
let xAxis = svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")");
xAxis.call(d3.axisBottom(this.xScale).tickFormat((d) => (d - 600) + "s"));
this.yAxis = svg.append("g")
.attr("class", "y-axis");
this.yAxis.call(d3.axisLeft(this.yScale));
}
getStats() {
this.apiService.getProjectStats(this.projectId).subscribe((data) => {
this.projectStats = data["stats"];
this.updateLine();
this.updatePie();
});
}
private updateLine() {
let newVal = {"y": this.projectStats["new_task_count"]};
let failedVal = {"y": this.projectStats["failed_task_count"]};
let closedVal = {"y": this.projectStats["closed_task_count"]};
//Adjust y axis
this.maxY = Math.max(newVal["y"], this.maxY);
this.yScale.domain([0, this.maxY]);
this.yAxis.call(d3.axisLeft(this.yScale));
this.newTaskPath
.attr("d", this.line)
.attr("transform", null);
this.failedTaskPath
.attr("d", this.line)
.attr("transform", null);
this.closedTaskPath
.attr("d", this.line)
.attr("transform", null);
//remove fist element
if (this.newTaskCounts.length >= this.range) {
this.newTaskCounts.shift();
this.newTaskPath
.transition()
.attr("transform", "translate(" + this.xScale(-1) + ")");
}
if (this.failedTaskCounts.length >= this.range) {
this.failedTaskCounts.shift();
this.failedTaskPath
.transition()
.attr("transform", "translate(" + this.xScale(-1) + ")");
}
if (this.closedTaskCounts.length >= this.range) {
this.closedTaskCounts.shift();
this.closedTaskPath
.transition()
.attr("transform", "translate(" + this.xScale(-1) + ")");
}
this.newTaskCounts.push(newVal);
this.failedTaskCounts.push(failedVal);
this.closedTaskCounts.push(closedVal);
}
private updatePie() {
this.statusData = [
{label: "New", count: this.projectStats["new_task_count"]},
{label: "Failed", count: this.projectStats["failed_task_count"]},
{label: "Closed", count: this.projectStats["closed_task_count"]},
];
this.assigneesData = _.map(this.projectStats["assignees"], assignedTask => {
return {
label: assignedTask["assignee"],
count: assignedTask["task_count"],
}
});
this.statusSvg.selectAll("path")
.data(this.pieFun(this.statusData));
this.statusPath
.attr('d', this.pieArc)
.attr('fill', (d) => this.statusColor(d.data.label));
this.assigneesSvg.selectAll("path")
.data(this.pieFun(this.assigneesData));
this.assigneesPath
.attr('d', this.pieArc)
.attr('fill', (d) => this.assigneesColor(d.data.label));
}
ngOnInit() {
this.statusData = [
{label: 'new', count: 0},
{label: 'failed', count: 0},
{label: 'closed', count: 0},
];
this.assigneesData = [
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
];
this.setupStatusPieChart();
this.setupAssigneesPieChart();
this.setupLine();
this.route.params.subscribe(params => {
this.projectId = params["id"];
this.getStats();
interval(1000).subscribe(() => {
// this.getStats()
})
}
)
ngOnInit(): void {
}
}

View File

@ -4,15 +4,15 @@
</mat-card-header>
<mat-card-content>
<mat-accordion>
<mat-expansion-panel *ngFor="let stats of projects">
<mat-expansion-panel *ngFor="let project of projects">
<mat-expansion-panel-header>
<mat-panel-title>{{stats.project.id}}: {{stats.project.name}}</mat-panel-title>
<mat-panel-description>{{stats.project.motd}}</mat-panel-description>
<mat-panel-title>{{project.id}}: {{project.name}}</mat-panel-title>
<mat-panel-description>{{project.motd}}</mat-panel-description>
</mat-expansion-panel-header>
<pre>{{stats.project | json}}</pre>
<pre>{{project | json}}</pre>
<div style="display: flex;">
<a [routerLink]="'/project/' + stats.project.id">Dashboard</a>
<a [routerLink]="'/project/' + stats.project.id + '/update'">Update</a>
<a [routerLink]="'/project/' + project.id">Dashboard</a>
<a [routerLink]="'/project/' + project.id + '/update'">Update</a>
</div>
</mat-expansion-panel>
</mat-accordion>

View File

@ -1,5 +1,6 @@
import {Component, OnInit} from '@angular/core';
import {ApiService} from "../api.service";
import {Project} from "../models/project";
@Component({
selector: 'app-project-list',
@ -11,14 +12,14 @@ export class ProjectListComponent implements OnInit {
constructor(private apiService: ApiService) {
}
projects: any[];
projects: Project[];
ngOnInit() {
this.getProjects()
}
getProjects() {
this.apiService.getProjects().subscribe(data => this.projects = data["stats"]);
this.apiService.getProjects().subscribe(data => this.projects = data["projects"]);
}
}

View File

@ -17,7 +17,7 @@ export class UpdateProjectComponent implements OnInit {
private router: Router) {
}
private project: Project;
project: Project;
private projectId: number;
ngOnInit() {

View File

@ -0,0 +1,6 @@
{
"nav": {
"title": "task_tracker",
"langSelect": "Language"
}
}

View File

@ -0,0 +1,6 @@
{
"nav": {
"title": "task_tracker (fr)",
"langSelect": "Langue"
}
}