* basic canban board now functional with wip limit display and auto refresh every 5minutes
This commit is contained in:
parent
f7bb463bd4
commit
4f64981cd6
58
deploy.php
Normal file
58
deploy.php
Normal 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
24
htaccess
Normal 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
22
semantic.json
Normal 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"
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
8
src/app/kanban/shared/priority-color.pipe.spec.ts
Normal file
8
src/app/kanban/shared/priority-color.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { PriorityColorPipe } from './priority-color.pipe';
|
||||
|
||||
describe('PriorityColorPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new PriorityColorPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
32
src/app/kanban/shared/priority-color.pipe.ts
Normal file
32
src/app/kanban/shared/priority-color.pipe.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user