More work on UI

This commit is contained in:
simon987 2019-01-23 19:38:33 -05:00
parent cbd32daf02
commit 1d656099f5
31 changed files with 288 additions and 131 deletions

View File

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

View File

@ -33,6 +33,12 @@ type GetProjectStatsResponse struct {
Stats *storage.ProjectStats `json:"stats,omitempty"`
}
type GetAllProjectsStatsResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
Stats *[]storage.ProjectStats `json:"stats,omitempty"`
}
func (api *WebAPI) ProjectCreate(r *Request) {
createReq := &CreateProjectRequest{}
@ -125,3 +131,13 @@ func (api *WebAPI) ProjectGetStats(r *Request) {
}, 404)
}
}
func (api *WebAPI) ProjectGetAllStats(r *Request) {
stats := api.Database.GetAllProjectsStats()
r.OkJson(GetAllProjectsStatsResponse{
Ok: true,
Stats: stats,
})
}

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
export INSTALL_DIR="/home/drone/task_tracker"
mkdir ${INSTALL_DIR} 2> /dev/null
# Gogs
if [[ ! -d "${INSTALL_DIR}/gogs" ]]; then
wget "https://dl.gogs.io/0.11.79/gogs_0.11.79_linux_amd64.tar.gz"
tar -xzf "gogs_0.11.79_linux_amd64.tar.gz" -C ${INSTALL_DIR}
rm "gogs_0.11.79_linux_amd64.tar.gz"
fi
# Postgres
su - postgres -c "createuser task_tracker"
su - postgres -c "dropdb gogs"
su - postgres -c "createdb gogs"

View File

@ -1,7 +1,6 @@
package storage
import (
"database/sql"
"encoding/json"
"github.com/Sirupsen/logrus"
"src/task_tracker/config"
@ -44,12 +43,6 @@ func (database *Database) SetupLoggerHook() {
func (database *Database) GetLogs(since int64, level logrus.Level) *[]LogEntry {
db := database.getDB()
logs := getLogs(since, level, db)
return logs
}
func getLogs(since int64, level logrus.Level, db *sql.DB) *[]LogEntry {
var logs []LogEntry

View File

@ -102,12 +102,6 @@ func scanProject(row *sql.Row) (*Project, error) {
func (database *Database) GetProjectWithRepoName(repoName string) *Project {
db := database.getDB()
project := getProjectWithRepoName(repoName, db)
return project
}
func getProjectWithRepoName(repoName string, db *sql.DB) *Project {
row := db.QueryRow(`SELECT * FROM project WHERE LOWER(git_repo)=$1`, strings.ToLower(repoName))
project, err := scanProject(row)
@ -124,10 +118,6 @@ func getProjectWithRepoName(repoName string, db *sql.DB) *Project {
func (database *Database) UpdateProject(project *Project) {
db := database.getDB()
updateProject(project, db)
}
func updateProject(project *Project, db *sql.DB) {
res, err := db.Exec(`UPDATE project
SET (priority, name, clone_url, git_repo, version, motd) = ($1,$2,$3,$4,$5,$6) WHERE id=$7`,
@ -147,13 +137,6 @@ func updateProject(project *Project, db *sql.DB) {
func (database *Database) GetProjectStats(id int64) *ProjectStats {
db := database.getDB()
stats := getProjectStats(id, db)
return stats
}
func getProjectStats(id int64, db *sql.DB) *ProjectStats {
stats := ProjectStats{}
stats.Project = getProject(id, db)
@ -169,7 +152,7 @@ func getProjectStats(id int64, db *sql.DB) *ProjectStats {
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"id": id,
}).Warn("???") //todo
}).Trace("Get project stats: No task for this project")
return nil
}
@ -187,3 +170,36 @@ func getProjectStats(id int64, db *sql.DB) *ProjectStats {
return &stats
}
func (database Database) GetAllProjectsStats() *[]ProjectStats {
var statsList []ProjectStats
db := database.getDB()
rows, err := db.Query(`SELECT
SUM(CASE WHEN status='new' THEN 1 ELSE 0 END) newCount,
SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) failedCount,
SUM(CASE WHEN status='closed' THEN 1 ELSE 0 END) closedCount,
p.*
FROM task INNER JOIN project p on task.project = p.id
GROUP BY p.id`)
handleErr(err)
for rows.Next() {
stats := ProjectStats{}
p := &Project{}
err := rows.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount,
&p.Id, &p.Priority, &p.Motd, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version)
handleErr(err)
stats.Project = p
statsList = append(statsList, stats)
}
logrus.WithFields(logrus.Fields{
"statsList": statsList,
}).Trace("Get all projects stats")
return &statsList
}

View File

@ -20,12 +20,6 @@ type Task struct {
func (database *Database) SaveTask(task *Task, project int64) error {
db := database.getDB()
taskErr := saveTask(task, project, db)
return taskErr
}
func saveTask(task *Task, project int64, db *sql.DB) error {
res, err := db.Exec(`
INSERT INTO task (project, max_retries, recipe, priority)
@ -52,12 +46,6 @@ func saveTask(task *Task, project int64, db *sql.DB) error {
func (database *Database) GetTask(worker *Worker) *Task {
db := database.getDB()
task := getTask(worker, db)
return task
}
func getTask(worker *Worker, db *sql.DB) *Task {
row := db.QueryRow(`
UPDATE task
@ -111,12 +99,6 @@ func getTaskById(id int64, db *sql.DB) *Task {
func (database Database) ReleaseTask(id int64, workerId *uuid.UUID, success bool) bool {
db := database.getDB()
res := releaseTask(workerId, id, success, db)
return res
}
func releaseTask(workerId *uuid.UUID, id int64, success bool, db *sql.DB) bool {
var res sql.Result
var err error
@ -139,15 +121,9 @@ func releaseTask(workerId *uuid.UUID, id int64, success bool, db *sql.DB) bool {
return rowsAffected == 1
}
func (database *Database) GetTaskFromProject(worker *Worker, project int64) *Task {
func (database *Database) GetTaskFromProject(worker *Worker, projectId int64) *Task {
db := database.getDB()
task := getTaskFromProject(worker, project, db)
return task
}
func getTaskFromProject(worker *Worker, projectId int64, db *sql.DB) *Task {
row := db.QueryRow(`
UPDATE task
@ -158,7 +134,7 @@ func getTaskFromProject(worker *Worker, projectId int64, db *sql.DB) *Task {
FROM task
INNER JOIN project p on task.project = p.id
WHERE assignee IS NULL AND p.id=$2
ORDER BY p.priority DESC, task.priority DESC
ORDER BY task.priority DESC
LIMIT 1
)
RETURNING id`, worker.Id, projectId)

View File

@ -21,17 +21,6 @@ type Worker struct {
func (database *Database) SaveWorker(worker *Worker) {
db := database.getDB()
saveWorker(worker, db)
}
func (database *Database) GetWorker(id uuid.UUID) *Worker {
db := database.getDB()
worker := getWorker(id, db)
return worker
}
func saveWorker(worker *Worker, db *sql.DB) {
identityId := getOrCreateIdentity(worker.Identity, db)
@ -45,7 +34,9 @@ func saveWorker(worker *Worker, db *sql.DB) {
}).Trace("Database.saveWorker INSERT worker")
}
func getWorker(id uuid.UUID, db *sql.DB) *Worker {
func (database *Database) GetWorker(id uuid.UUID) *Worker {
db := database.getDB()
worker := &Worker{}
var identityId int64

View File

@ -10,7 +10,6 @@ func BenchmarkCreateTask(b *testing.B) {
resp := createProject(api.CreateProjectRequest{
Name: "BenchmarkCreateTask" + strconv.Itoa(b.N),
Priority: 1,
GitRepo: "benchmark_test" + strconv.Itoa(b.N),
Version: "f09e8c9r0w839x0c43",
CloneUrl: "http://localhost",

0
web/angular/e2e/protractor.conf.js Normal file → Executable file
View File

0
web/angular/e2e/src/app.e2e-spec.ts Normal file → Executable file
View File

0
web/angular/e2e/src/app.po.ts Normal file → Executable file
View File

0
web/angular/e2e/tsconfig.e2e.json Normal file → Executable file
View File

10
web/angular/src/app/api.service.ts Normal file → Executable file
View File

@ -12,10 +12,18 @@ export class ApiService {
}
getLogs() {
return this.http.get(this.url + "/logs");
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")
}
getProject(id: number) {
return this.http.get(this.url + "/project/get/" + id)
}
}

8
web/angular/src/app/app-routing.module.ts Normal file → Executable file
View File

@ -2,10 +2,16 @@ import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {LogsComponent} from "./logs/logs.component";
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 {UpdateProjectComponent} from "./update-project/update-project.component";
const routes: Routes = [
{path: "log", component: LogsComponent},
{path: "project", component: ProjectDashboardComponent}
{path: "projects", component: ProjectListComponent},
{path: "project/:id", component: ProjectDashboardComponent},
{path: "project/:id/update", component: UpdateProjectComponent},
{path: "new_project", component: CreateProjectComponent}
];
@NgModule({

0
web/angular/src/app/app.component.css Normal file → Executable file
View File

14
web/angular/src/app/app.component.html Normal file → Executable file
View File

@ -1,8 +1,12 @@
<mat-toolbar>
<a [routerLink]="''">Index</a>
<a [routerLink]="'log'">Logs</a>
<a [routerLink]="'project'">Project</a>
</mat-toolbar>
<!--<mat-toolbar>-->
<ul>
<li><a [routerLink]="''">Index</a></li>
<li><a [routerLink]="'log'">Logs</a></li>
<li><a [routerLink]="'project'">Project</a></li>
<li><a [routerLink]="'projects'">list</a></li>
<li><a [routerLink]="'new_project'">new project</a></li>
</ul>
<!--</mat-toolbar>-->
<router-outlet></router-outlet>

15
web/angular/src/app/app.module.ts Normal file → Executable file
View File

@ -7,6 +7,8 @@ import {LogsComponent} from './logs/logs.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {
MatAutocompleteModule,
MatButtonModule,
MatCardModule,
MatExpansionModule,
MatFormFieldModule,
@ -22,12 +24,19 @@ import {
import {ApiService} from "./api.service";
import {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';
@NgModule({
declarations: [
AppComponent,
LogsComponent,
ProjectDashboardComponent
ProjectDashboardComponent,
ProjectListComponent,
CreateProjectComponent,
UpdateProjectComponent
],
imports: [
BrowserModule,
@ -41,6 +50,10 @@ import {ProjectDashboardComponent} from './project-dashboard/project-dashboard.c
MatInputModule,
MatToolbarModule,
MatCardModule,
MatButtonModule,
MatAutocompleteModule,
ReactiveFormsModule,
FormsModule,
MatExpansionModule,
MatTreeModule,
BrowserAnimationsModule,

View File

@ -0,0 +1,4 @@
.mat-form-field {
width: 100%;
}

View File

@ -0,0 +1,30 @@
<mat-card>
<mat-card-title>Create new project</mat-card-title>
<mat-card-subtitle></mat-card-subtitle>
<mat-card-content>
<form>
<mat-form-field>
<input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Name">
</mat-form-field>
<mat-form-field>
<input type="text" matInput [(ngModel)]="project.clone_url" name="clone_url"
placeholder="Git clone url">
</mat-form-field>
<mat-form-field>
<input type="text" matInput [(ngModel)]="project.git_repo" name="clone_url"
placeholder='Full repository name (e.g. "simon987/task_tracker")'>
<mat-hint align="start">Changes on the <strong>master</strong> branch will be tracked if webhooks are
enabled
</mat-hint>
</mat-form-field>
<input type="hidden" name="version" value="{{project.version}}">
</form>
</mat-card-content>
<mat-card-actions>
<button>Create</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,20 @@
import {Component, OnInit} from '@angular/core';
import {Project} from "../models/project";
@Component({
selector: 'app-create-project',
templateUrl: './create-project.component.html',
styleUrls: ['./create-project.component.css']
})
export class CreateProjectComponent implements OnInit {
private project = new Project();
constructor() {
this.project.name = "test"
}
ngOnInit() {
}
}

View File

@ -1,25 +0,0 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {LogsComponent} from './logs.component';
describe('LogsComponent', () => {
let component: LogsComponent;
let fixture: ComponentFixture<LogsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LogsComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LogsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -40,7 +40,7 @@ export class LogsComponent implements OnInit {
private getLogs() {
this.apiService.getLogs().subscribe(
data => {
this.data.data = _.map(data, (entry) => {
this.data.data = _.map(data["logs"], (entry) => {
return <LogEntry>{
message: entry.message,
timestamp: moment.unix(entry.timestamp).toISOString(),

View File

@ -0,0 +1,9 @@
export class Project {
public priority: number;
public motd: string;
public name: string;
public clone_url: string;
public git_repo: string;
public version: string;
}

View File

@ -4,6 +4,7 @@ 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',
@ -12,7 +13,9 @@ import {ApiService} from "../api.service";
})
export class ProjectDashboardComponent implements OnInit {
private projectId;
projectStats;
private pieWidth = 360;
private pieHeight = 360;
private pieRadius = Math.min(this.pieWidth, this.pieHeight) / 2;
@ -47,7 +50,7 @@ export class ProjectDashboardComponent implements OnInit {
private assigneesPath: any;
private assigneesSvg: any;
constructor(private apiService: ApiService) {
constructor(private apiService: ApiService, private route: ActivatedRoute) {
}
setupStatusPieChart() {
@ -165,7 +168,7 @@ export class ProjectDashboardComponent implements OnInit {
}
getStats() {
this.apiService.getProjectStats(2).subscribe((data) => {
this.apiService.getProjectStats(this.projectId).subscribe((data) => {
this.projectStats = data["stats"];
@ -259,9 +262,13 @@ export class ProjectDashboardComponent implements OnInit {
this.setupAssigneesPieChart();
this.setupLine();
this.getStats();
interval(1000).subscribe(() => {
this.getStats()
})
this.route.params.subscribe(params => {
this.projectId = params["id"];
this.getStats();
interval(1000).subscribe(() => {
// this.getStats()
})
}
)
}
}

View File

@ -0,0 +1,19 @@
<mat-card>
<mat-card-header>
<mat-card-title>Projects</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-accordion>
<mat-expansion-panel *ngFor="let stats 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-expansion-panel-header>
<pre>{{stats.project | json}}</pre>
<div style="display: flex;">
<a [routerLink]="'/project/' + stats.project.id">Dashboard</a>
</div>
</mat-expansion-panel>
</mat-accordion>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,24 @@
import {Component, OnInit} from '@angular/core';
import {ApiService} from "../api.service";
@Component({
selector: 'app-project-list',
templateUrl: './project-list.component.html',
styleUrls: ['./project-list.component.css']
})
export class ProjectListComponent implements OnInit {
constructor(private apiService: ApiService) {
}
projects: any[];
ngOnInit() {
this.getProjects()
}
getProjects() {
this.apiService.getProjects().subscribe(data => this.projects = data["stats"]);
}
}

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"sourceMap": true
},
"exclude": [
"node_modules"
]
}

View File

@ -0,0 +1,4 @@
.mat-form-field {
width: 100%;
}

View File

@ -0,0 +1,29 @@
<mat-card>
<mat-card-title>Update project</mat-card-title>
<mat-card-subtitle>Changes are saved in real time</mat-card-subtitle>
<mat-card-content>
<form>
<mat-form-field>
<input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Name">
</mat-form-field>
<mat-form-field>
<textarea matInput placeholder="Message of the day"></textarea>
</mat-form-field>
<mat-form-field>
<input type="text" matInput [(ngModel)]="project.clone_url" name="clone_url"
placeholder="Git clone url">
</mat-form-field>
<mat-form-field>
<input type="text" matInput [(ngModel)]="project.git_repo" name="clone_url"
placeholder='Full repository name (e.g. "simon987/task_tracker")'>
<mat-hint align="start">Changes on the <strong>master</strong> branch will be tracked if webhooks are
enabled
</mat-hint>
</mat-form-field>
<input type="hidden" name="version" value="{{project.version}}">
</form>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,40 @@
import {Component, OnInit} from '@angular/core';
import {Project} from "../models/project";
import {ApiService} from "../api.service";
import {ActivatedRoute} from "@angular/router";
@Component({
selector: 'app-update-project',
templateUrl: './update-project.component.html',
styleUrls: ['./update-project.component.css']
})
export class UpdateProjectComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute) {
}
private project: Project;
private projectId: number;
ngOnInit() {
this.route.params.subscribe(params => {
this.projectId = params["id"];
this.getProject();
})
}
private getProject() {
this.apiService.getProject(this.projectId).subscribe(data => {
this.project = <Project>{
name: data["project"]["name"],
clone_url: data["project"]["clone_url"],
git_repo: data["project"]["git_repo"],
motd: data["project"]["motd"],
priority: data["project"]["priority"],
version: data["project"]["version"]
}
})
}
}