Skip to content

Commit

Permalink
Merge branch 'feat-build-cards' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
Ephigenia committed Jun 14, 2018
2 parents 729439a + 471f277 commit a32628d
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 37 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Simple angular driven application which shows [circleci](https://circleci.com) b
- optional grouping of jobs from the same workflow
- circleci token stored in localstorage, can also be injected via GET-parameter (`?apiToken=<value>`)
- inject other config settings via GET parameters (f.e. `?refreshInterval=20&groupWorkflows=true&fontSize=20`)
- gitlab: add multiple gitlab projects and show them too
- message when device/client goes offline

Other ideas & planned features can be found in the [wiki](https://github.com/Ephigenia/circleboard2/wiki). If something doesn’t work please [create an issue](https://github.com/ephigenia/circleboard2/issues).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"start": "ng serve",
"start:heroku": "http-server dist -c-1",
"test": "ng test --code-coverage",
"tdd": "ng test --watch --reporters=progress",
"tdd": "ng test --watch",
"update": "npm-check --update --specials=bin",
"version": "npm run changelog && git add CHANGELOG.md",
"version:recommend": "conventional-recommended-bump --preset angular"
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { BuildListComponent } from './build-list/build-list.component';
import { BuildListItemComponent } from './build-list-item/build-list-item.component';
import { CircleCiService } from './circle-ci.service';
import { BoardConfigService } from './board-config.service';
import { GitlabCiService } from './gitlab-ci.service';
import { NavBarComponent } from './nav-bar/nav-bar.component';
import { RecentBuildsComponent } from './recent-builds/recent-builds.component';

Expand All @@ -31,6 +32,7 @@ import { RecentBuildsComponent } from './recent-builds/recent-builds.component';
],
providers: [
CircleCiService,
GitlabCiService,
BoardConfigService,
],
bootstrap: [AppComponent]
Expand Down
10 changes: 10 additions & 0 deletions src/app/board-config.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ describe('BoardConfig', () => {
});
});
describe('merge', () => {

it('converts the gitlab string to single project configs', () => {
const csv = 'org/project,token,,%0Aorg2/project2,token2,baseUrl,%0A';
config.merge({ gitlab: csv });
expect(config.gitlabProjects.length).toEqual(2);
expect(config.gitlabProjects[0].name).toEqual('org/project');
expect(config.gitlabProjects[0].token).toEqual('token');
expect(config.gitlabProjects[0].baseUrl).toEqual(null);
});

it('doesn’t change any config vars when the object is empty', () => {
config.merge({});
expect(config.apiToken).toEqual('initial-api-token');
Expand Down
20 changes: 20 additions & 0 deletions src/app/board-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class BoardConfig {
private _groupWorkflows = false;
private _refreshInterval = 20;
private _fontSize = 16;
public gitlabProjects = [];

get groupWorkflows() {
return this._groupWorkflows;
Expand Down Expand Up @@ -43,17 +44,36 @@ export class BoardConfig {
}
}

public parseGitLabString(csvString: string): any[] {
return decodeURIComponent(csvString)
.split(/[\n\r]/)
.map(line => line.split(/[,;]/))
.filter(v => v && v.length > 1)
.map((csvRow) => {
return {
name: csvRow[0],
token: csvRow[1],
baseUrl: csvRow[2] || null
}
});
}

public merge(config: object) {
if (config['groupWorkflows']) {
this.groupWorkflows = config['groupWorkflows'];
}
this.refreshInterval = config['refreshInterval'] || this.refreshInterval;
this.apiToken = config['apiToken'] || this.apiToken;
this.fontSize = config['fontSize'] || this.fontSize;
// convert gitlab csv string
if (typeof config['gitlab'] === 'string') {
this.gitlabProjects = this.parseGitLabString(config['gitlab']);
}
return this;
}
}


@Injectable()
export class BoardConfigService {

Expand Down
130 changes: 103 additions & 27 deletions src/app/board-config/board-config.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,16 @@
<h1>Board-Config</h1>
</header>
<section>

<p class="alert alert-info">
All the configuration variables can be set via GET parameters in the URL.<br>
For example: <code>{{baseUrl}}/?refreshInterval=20&fontSize=22</code>
For example: <code>{{baseUrl}}/?refreshInterval=20&fontSize=22</code><br>
Each config setting contains a short description on how to set it via URL.
</p>

<form (ngSubmit)="submit()" #configForm="ngForm">
<fieldset>
<div class="form-group">
<label for="boardConfigApiToken">
CircleCi API-Key
</label>
<input
type="text"
name="apiToken"
class="form-control"
id="boardConfigApiToken"
aria-describedby="boardConfigApiTokenHelp"
placeholder="enter the api key"
[ngClass]="{
'form-control': true,
'is-invalid': apiToken.invalid,
'is-valid': apiToken.valid
}"
required
pattern="[A-Fa-f0-9]{40}"
[(ngModel)]="config.apiToken"
#apiToken="ngModel"
>
<small id="boardConfigApiTokenHelp" class="form-text text-muted">
Optain a application specific api token in your <a href="https://circleci.com/account/api" target="_blank" rel="external">CircleCi Account Settings</a>.<br>
It’s possible ot inject the token value via GET URL parameter: <code>?apiToken=&lt;value&gt;</code>.
</small>
</div>
<legend>General</legend>
<div class="form-group">
<label for="boardConfigApiToken">
Refresh Interval
Expand Down Expand Up @@ -74,6 +52,104 @@ <h1>Board-Config</h1>
It’s possible ot inject the token value via GET URL parameter. For Example: <code>?groupWorkflows=&lt;yes&gt;</code>.
</small>
</div>
</fieldset>

<fieldset>
<legend>CircleCi</legend>
<div class="form-group">
<label for="boardConfigApiToken">
CircleCi API-Key
</label>
<input
type="text"
name="apiToken"
class="form-control"
id="boardConfigApiToken"
aria-describedby="boardConfigApiTokenHelp"
placeholder="enter the api key"
[ngClass]="{
'form-control': true,
'is-invalid': apiToken.invalid,
'is-valid': apiToken.valid
}"
required
pattern="[A-Fa-f0-9]{40}"
[(ngModel)]="config.apiToken"
#apiToken="ngModel"
>
<small id="boardConfigApiTokenHelp" class="form-text text-muted">
Optain a application specific api token in your <a href="https://circleci.com/account/api" target="_blank" rel="external">CircleCi Account Settings</a>.<br>
It’s possible ot inject the token value via GET URL parameter: <code>?apiToken=&lt;value&gt;</code>.
</small>
</div>
</fieldset>

<fieldset class="form-group">
<legend>
GitLab
<button class="btn btn-sm btn-outline-primary" (click)="addEmptyGitlabProject()">
<i class="fa fa-plus"></i>
</button>
</legend>
<p class="alert alert-info">
Setting the gitlab projects via GET variable is a bit more complicated as each project needs at least
the project’s name and an access token defined. Therefore the GET variable works like a csv list of
projects.<br>
<br>
Make sure you encode all entities like line breaks (<code>%0A</code>) and slashes (<code>%2F</code>) properly.<br>
<code>?gitlab=org%2Fproject,token,baseUrl%0Aorg%2Fproject,token,baseUrl</code>
</p>
<!-- one card for the settings of each project -->
<div class="card"
*ngFor="let gitlabProject of config.gitlabProjects; let i = index;"
[class.border-warning]="!(gitlabProject.name && gitlabProject.token)"
>
<div class="card-header" [class.text-warning]="!(gitlabProject.name && gitlabProject.token)">
Project
<ul class="pull-right nav nav-pills card-header-pills">
<li class="nav-item">
<button class="btn btn-sm btn-outline-primary" (click)="removeGitLabProject(i)">
<i class="fa fa-minus"></i>
</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="row">
<div class="col">
<div class="form-group">
<label>
Project Name
</label>
<input class="form-control" placeholder="project name" type="text"
name="gitlabProjectName{{i}}" [(ngModel)]="config.gitlabProjects[i].name" />
<small class="form-text text-muted">project name including organization and repo name</small>
</div>
</div>
<div class="col">
<div class="form-group">
<label>Access-Token</label>
<input class="form-control" placeholder="project" type="text"
name="gitlabProjectToken{{i}}" [(ngModel)]="config.gitlabProjects[i].token" />
<small class="form-text text-muted">
personal access token with the "api" permission generated in <a href="https://gitlab.com/profile/personal_access_tokens">gitlab profile settings</a>
</small>
</div>
</div>
<div class="col">
<div class="form-group">
<label>Base-URL</label>
<input class="form-control" placeholder="gitlab baseurl" type="text"
name="gitlabProjectBaseUrl{{i}}" [(ngModel)]="config.gitlabProjects[i].baseUrl" />
<small class="form-text text-muted">base url (defaults to: <code>gitlab.com</code>)</small>
</div>
</div>
</div>
</div>
</div>
</fieldset>

<fieldset>
<button type="submit" class="btn btn-primary" [disabled]="!configForm.form.valid">
Save
</button>
Expand Down
25 changes: 22 additions & 3 deletions src/app/board-config/board-config.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component } from '@angular/core';

import { BoardConfigService, BoardConfig } from '../board-config.service';
import { GitLabProjectListItem } from './../gitlab-ci.service';

@Component({
selector: 'app-board-config',
Expand All @@ -10,16 +11,34 @@ export class BoardConfigComponent {

public config: BoardConfig;

public baseUrl: string;

public constructor(
private configService: BoardConfigService
) {
this.config = this.configService.read();
this.baseUrl = `${window.location.origin}`;
}

public submit() {
this.configService.save(this.config);
}

public addEmptyGitlabProject() {
let template = <GitLabProjectListItem>{
name: '',
token:'',
baseUrl: ''
};
if (this.config.gitlabProjects.length) {
template = <GitLabProjectListItem> Object.assign(
{},
this.config.gitlabProjects[this.config.gitlabProjects.length - 1]
);
template.name = '';
}
this.config.gitlabProjects.push(template);
return true;
}

public removeGitLabProject(index) {
return this.config.gitlabProjects.splice(index, 1);
}
}
16 changes: 14 additions & 2 deletions src/app/build-list-item/build-list-item.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,19 @@
<i class="fa fa-user-circle-o" aria-hidden="true" *ngIf="item.committer_name"></i>
{{ item.committer_name }}
</td>
<td *ngIf="item.lifecycle === 'finished'">

<!-- gitlab states -->
<td *ngIf="item.source === 'gitlab' && item.lifecycle === 'success'">
<span class="badge text-uppercase badge-success">{{ item.outcome }}</span>
</td>
<td *ngIf="item.source === 'gitlab' && item.lifecycle === 'failed'">
<span class="badge text-uppercase badge-danger">{{ item.outcome }}</span>
</td>
<td *ngIf="item.source === 'gitlab' && item.lifecycle === 'skipped'">
<span class="badge text-uppercase badge-warning">{{ item.outcome }}</span>
</td>

<td *ngIf="item.source !== 'gitlab' && item.lifecycle === 'finished'">
<span class="badge text-uppercase"
[ngClass]="{
'badge-success': ['success'].indexOf(item.outcome) > -1,
Expand All @@ -65,7 +77,7 @@
{{ item.outcome }}
</span>
</td>
<td *ngIf="item.lifecycle !== 'finished'">
<td *ngIf="item.source !== 'gitlab' && item.lifecycle !== 'finished'">
<span class="badge text-uppercase"
[ngClass]="{
'badge-secondary': ['queued', 'scheduled', 'not_run', 'not_running'].indexOf(item.lifecycle) > -1,
Expand Down
65 changes: 65 additions & 0 deletions src/app/gitlab-ci.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';

import { map } from 'rxjs/operators';

export interface GitLabProjectListItem {
name: string,
token: string,
baseUrl: string|null,
}

@Injectable()
export class GitlabCiService {

constructor(
private http: HttpClient
) { }

public getProjectBuilds(
project: string,
token: string,
baseUrl: string = ''
) {
if (!baseUrl) {
baseUrl = 'https://gitlab.com/api/v4/projects';
}
if (!token) {
throw new Error('gitlab ci requires token to be working');
}
const url = [baseUrl, encodeURIComponent(project), 'jobs'].join('/');
const options = {
headers: {
'PRIVATE-TOKEN': token
}
};
return this.http.get(url, options).pipe(
map((builds: any[]) => {
return builds.map((build) => this.transformBuild(build, project, baseUrl));
}));
}

public transformBuild(build, project, baseUrl) {
return {
source: 'gitlab',
vcs_revision: build.commit.id,
created_at: build.created_at,
start_time: build.started_at,
stop_time: build.finished_at,
build_time_millis: build.duration * 1000,
outcome: build.status,
lifecycle: build.status,
compare: '',
build_url: '',
vcs_url: '',
reponame: project,
build_num: build.id,
subject: build.commit.title,
committer_email: build.commit.committer_email,
committer_name: build.commit.committer_name,
committer_date: build.commit.committer_date,
};
}

}
Loading

0 comments on commit a32628d

Please sign in to comment.