* basic canban board now functional with wip limit display and auto refresh every 5minutes

This commit is contained in:
Dávid Danyi 2017-08-15 16:39:19 +02:00
parent f7bb463bd4
commit 4f64981cd6
14 changed files with 285 additions and 30 deletions

58
deploy.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace Deployer;
require 'recipe/common.php';
set('ssh_type', 'native');
set('ssh_multiplexing', true);
// Configuration
// set('repository', 'git@domain.com:username/repository.git');
set('shared_files', []);
set('shared_dirs', []);
set('writable_dirs', []);
set('keep_releases', 3);
set('default_stage', 'production');
// Servers
server('vasgyuro', 'vasgyuro.tsp')
->stage('production')
->user('edvidan')
->forwardAgent()
->set('ng_basehref', '/taurus-tv/')
->set('ng_target', 'production')
->set('ng_environment', 'prod')
->set('env_vars', 'NODE_ENV=production')
->set('deploy_path', '/home/edvidan/applications/taurus-tv');
// Tasks
desc('Prepare release');
task('deploy:ng-prepare', function() {
runLocally("ng build --base-href={{ng_basehref}} --target={{ng_target}} --environment={{ng_environment}}");
runLocally("tar -cJf dist.tar.xz dist");
});
desc('Upload release');
task('deploy:ng-upload', function() {
upload("dist.tar.xz", "{{release_path}}/dist.tar.xz");
run("tar -C {{release_path}} -xJf {{release_path}}/dist.tar.xz");
run("rm -f {{release_path}}/dist.tar.xz");
runLocally("rm -rf dist.tar.xz dist");
upload("htaccess", "{{release_path}}/dist/.htaccess");
});
desc('Deploy your project');
task('deploy', [
'deploy:prepare',
'deploy:lock',
'deploy:release',
'deploy:ng-prepare',
'deploy:ng-upload',
'deploy:shared',
'deploy:clear_paths',
'deploy:symlink',
'deploy:unlock',
'cleanup',
]);
after('deploy', 'success');

24
htaccess Normal file
View File

@ -0,0 +1,24 @@
RewriteEngine On
# The following rule tells Apache that if the requested filename
# exists, simply serve it.
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
# The following rewrites all other queries to index.php. The
# condition ensures that if you are using Apache aliases to do
# mass virtual hosting, the base path will be prepended to
# allow proper resolution of the index.php file; it will work
# in non-aliased environments as well, providing a safe, one-size
# fits all solution.
RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteRule ^(.*)$ %{ENV:BASE}index.html [NC,L]
<Limit GET POST PUT DELETE HEAD OPTIONS>
Require all granted
</Limit>
<LimitExcept GET POST PUT DELETE HEAD OPTIONS>
Require all denied
</LimitExcept>

22
semantic.json Normal file
View File

@ -0,0 +1,22 @@
{
"base": "semantic/",
"paths": {
"source": {
"config": "src/theme.config",
"definitions": "src/definitions/",
"site": "src/site/",
"themes": "src/themes/"
},
"output": {
"packaged": "dist/",
"uncompressed": "dist/components/",
"compressed": "dist/components/",
"themes": "dist/themes/"
},
"clean": "dist/"
},
"permission": false,
"autoInstall": true,
"rtl": false,
"version": "2.2.11"
}

View File

@ -1,10 +1,31 @@
import { Component } from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Subscription} from "rxjs/Subscription";
import {TimerObservable} from "rxjs/observable/TimerObservable";
import {KanbanService} from "./kanban/shared/kanban.service";
const RENEW_TIMER_INITIAL = 300000;
const RENEW_TIMER_PERIOD = 300000;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
export class AppComponent implements OnInit, OnDestroy{
title = 'app';
authRenewTimer: Subscription;
constructor( private kanbanService: KanbanService ) {}
public ngOnInit() {
let timer = TimerObservable.create(RENEW_TIMER_INITIAL, RENEW_TIMER_PERIOD);
this.authRenewTimer = timer.subscribe(() => {
this.kanbanService.reload();
});
}
public ngOnDestroy() {
this.authRenewTimer.unsubscribe();
}
}

View File

@ -1,13 +1,12 @@
<div class="ui main fullwide-container">
<div class="ui grid">
<div class="six wide column">
<app-kanban-entry-item [kanbanEntries]="kanbanBoard.inbox"></app-kanban-entry-item>
</div>
<div class="six wide column">
<app-kanban-entry-item [kanbanEntries]="kanbanBoard.inProgress"></app-kanban-entry-item>
</div>
<div class="four wide column">
<app-kanban-entry-item [kanbanEntries]="kanbanBoard.verification"></app-kanban-entry-item>
</div>
<div app-kanban-entry-item class="four wide column"
[kanbanEntries]="kanbanBoard.inbox"></div>
<div app-kanban-entry-item class="four wide column" [ngClass]="inprogressWipClass"
[kanbanEntries]="kanbanBoard.inProgress"></div>
<div app-kanban-entry-item class="four wide column" [ngClass]="verificationWipClass"
[kanbanEntries]="kanbanBoard.verification"></div>
<div app-kanban-entry-item class="four wide column"
[kanbanEntries]="kanbanBoard.done"></div>
</div>
</div>

View File

@ -5,6 +5,10 @@ import {ActivatedRoute} from '@angular/router';
import {
KanbanBoard
} from "../shared";
import {KanbanService} from "../shared/kanban.service";
const WIP_LIMIT_INPROGRESS = 12;
const WIP_LIMIT_VERIFICATION = 8;
@Component({
selector: 'app-kanban-board',
@ -13,10 +17,9 @@ import {
})
export class KanbanBoardComponent implements OnInit {
public kanbanBoard: KanbanBoard = new KanbanBoard;
constructor(private titleService: Title,
private route: ActivatedRoute) {
private route: ActivatedRoute,
private kanbanService: KanbanService) {
}
ngOnInit() {
@ -24,4 +27,23 @@ export class KanbanBoardComponent implements OnInit {
this.route.data.subscribe((data: { kanbanBoard: KanbanBoard }) => this.kanbanBoard = data.kanbanBoard);
}
get kanbanBoard(): KanbanBoard {
return this.kanbanService.kanbanBoard;
}
set kanbanBoard(kanbanBoard: KanbanBoard) {
this.kanbanService.kanbanBoard = kanbanBoard;
}
get inprogressWipClass() {
return {
'over-wip': this.kanbanBoard.inProgress.length > WIP_LIMIT_INPROGRESS,
};
}
get verificationWipClass() {
return {
'over-wip': this.kanbanBoard.verification.length > WIP_LIMIT_VERIFICATION,
};
}
}

View File

@ -0,0 +1,25 @@
.task-description {
font-size: 14pt;
line-height: 1.25em;
/*text-align: justify;*/
}
.ui.jira-avatar.image {
width: 45px;
height: auto;
/*font-size: 1em;*/
margin-right: 4px;
margin-bottom: 0;
}
.ui.jira-avatar.image > img {
border-radius: 4px;
}
.ui.divided.items > .item:first-child {
border-top: 0;
}
.ui.divided.items > .item {
border-top: 1px solid rgba(250, 250, 250, 0.25);
}

View File

@ -1,15 +1,11 @@
<div class="ui divided items">
<div class="item" *ngFor="let kanbanEntry of kanbanEntries">
<div class="ui tiny image">
<div class="content">
<div class="task-description">
<div class="ui jira-avatar floated image">
<img src="{{avatarUrl(kanbanEntry.assignee?.avatar)}}">
</div>
<div class="content">
<a class="header">{{kanbanEntry.key}}</a>
<div class="description">
<p>{{kanbanEntry.summary}}</p>
</div>
<div class="extra">
Additional Details
<span [innerHTML]="kanbanEntry.summary|priorityColor"></span>
</div>
</div>
</div>

View File

@ -3,8 +3,10 @@ import {Component, Input, OnInit} from '@angular/core';
import {environment} from "../../../environments/environment";
import {KanbanEntry} from "../shared/kanban-entry.model";
const DEFAULT_AVATAR = '/assets/riddler.png';
@Component({
selector: 'app-kanban-entry-item',
selector: 'app-kanban-entry-item,[app-kanban-entry-item]',
templateUrl: './kanban-entry-item.component.html',
styleUrls: ['./kanban-entry-item.component.css']
})
@ -16,10 +18,6 @@ export class KanbanEntryItemComponent implements OnInit {
ngOnInit() {}
public avatarUrl(avatarPath: string): string {
try {
return environment.apiUri + avatarPath;
} catch (e) {
return "";
}
return environment.apiUri + ( avatarPath ? avatarPath : DEFAULT_AVATAR );
}
}

View File

@ -7,6 +7,7 @@ import { KanbanBoardComponent } from './kanban-board/kanban-board.component';
import { KanbanService } from './shared/kanban.service';
import { KanbanEntryItemComponent } from './kanban-entry-item/kanban-entry-item.component';
import { KanbanEntryCardComponent } from './kanban-entry-card/kanban-entry-card.component';
import { PriorityColorPipe } from './shared/priority-color.pipe';
@NgModule({
imports: [
@ -17,6 +18,7 @@ import { KanbanEntryCardComponent } from './kanban-entry-card/kanban-entry-card.
KanbanBoardComponent,
KanbanEntryItemComponent,
KanbanEntryCardComponent,
PriorityColorPipe,
],
providers: [
KanbanService,

View File

@ -13,6 +13,8 @@ import {
export class KanbanService {
private url = environment.apiUri + '/api/kanban';
private cachedKanbanBoard: KanbanBoard = new KanbanBoard();
constructor(private httpService: Http) {}
public getList(): Observable<KanbanBoard> {
@ -22,4 +24,17 @@ export class KanbanService {
public resolve(route: ActivatedRouteSnapshot): Promise<KanbanBoard> {
return this.getList().toPromise().then(result => result ? result : false);
}
public reload() {
this.getList().subscribe(result => this.cachedKanbanBoard = result);
}
get kanbanBoard(): KanbanBoard {
return this.cachedKanbanBoard;
}
set kanbanBoard(kanbanBoard: KanbanBoard) {
this.cachedKanbanBoard = kanbanBoard;
}
}

View File

@ -0,0 +1,8 @@
import { PriorityColorPipe } from './priority-color.pipe';
describe('PriorityColorPipe', () => {
it('create an instance', () => {
const pipe = new PriorityColorPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'priorityColor'
})
export class PriorityColorPipe implements PipeTransform {
transform(value: any, args?: any): any {
let mhrMatch = /(\[.*mhr\])/ig;
value = value.replace(mhrMatch, (fullMatch: string, mhrMatched: string) => {
return `<span class="match-mhr">${mhrMatched}</span> `;
});
let sMatch = /(\[s\])/ig;
value = value.replace(sMatch, (fullMatch: string, mhrMatched: string) => {
return `<span class="match-s">${mhrMatched}</span> `;
});
let mMatch = /(\[m\])/ig;
value = value.replace(mMatch, (fullMatch: string, mhrMatched: string) => {
return `<span class="match-m">${mhrMatched}</span> `;
});
let lMatch = /(\[l\])/ig;
value = value.replace(lMatch, (fullMatch: string, mhrMatched: string) => {
return `<span class="match-l">${mhrMatched}</span> `;
});
let xlMatch = /(\[xl\])/ig;
value = value.replace(xlMatch, (fullMatch: string, mhrMatched: string) => {
return `<span class="match-xl">${mhrMatched}</span> `;
});
return value;
}
}

View File

@ -1,9 +1,42 @@
/* You can add global styles to this file, and also import other style files */
body {
background-color: #FFFFFF;
background-color: #303030 !important;
color: #cccccc !important;
margin-top: 1em !important;
margin-bottom: 1em !important;
overflow: hidden;
}
.ui.fullwide-container {
margin-left: 1em;
margin-right: 1em;
}
.match-mhr {
text-justify: none;
color: red;
}
.match-s {
text-justify: none;
color: #00b5ad;
}
.match-m {
text-justify: none;
color: #0ea432;
}
.match-l {
text-justify: none;
color: yellow;
}
.match-xl {
text-justify: none;
color: coral;
}
.over-wip {
background-color: rgba(194,59,34, 0.3);
}