diff --git a/src/api/db/access-token.ts b/src/api/db/access-token.ts new file mode 100644 index 000000000..4a1622390 --- /dev/null +++ b/src/api/db/access-token.ts @@ -0,0 +1,27 @@ +import { AccessToken } from './model'; + +export function getAccessTokens(): Promise { + return new Promise((resolve, reject) => { + new AccessToken().fetchAll({ withRelated: ['user'] }) + .then(tokens => { + if (!tokens) { + reject(tokens); + } + + const result = tokens.toJSON().map(token => { + delete token.token; + delete token.user.password; + return token; + }); + + resolve(result); + }); + }); +} + +export function insertAccessToken(data: any): Promise { + return new Promise((resolve, reject) => { + new AccessToken().save(data, { method: 'insert' }) + .then(token => !token ? reject(token) : resolve(token.toJSON())); + }); +} diff --git a/src/api/db/migrations.ts b/src/api/db/migrations.ts index d5f882004..012b49e13 100644 --- a/src/api/db/migrations.ts +++ b/src/api/db/migrations.ts @@ -38,9 +38,8 @@ export function create(): Promise { t.string('user_avatar_url'); t.string('user_url'); t.string('user_html_url'); - t.string('username'); - t.string('password'); - t.string('access_token'); + t.integer('access_tokens_id'); + t.foreign('access_tokens_id').references('access_tokens.id'); t.json('data'); t.timestamps(); })) diff --git a/src/api/db/model.ts b/src/api/db/model.ts index 46a40b6c0..52e58df49 100644 --- a/src/api/db/model.ts +++ b/src/api/db/model.ts @@ -4,12 +4,20 @@ import { Bookshelf } from './config'; export class User extends Bookshelf.Model { get tableName() { return 'users'; } get hasTimestamps() { return true; } + access_tokens() { return this.hasMany(AccessToken, 'users_id'); } +} + +export class AccessToken extends Bookshelf.Model { + get tableName() { return 'access_tokens'; } + get hasTimestamps() { return true; } + user() { return this.belongsTo(User, 'users_id'); } } export class Repository extends Bookshelf.Model { get tableName() { return 'repositories'; } get hasTimestamps() { return true; } builds() { return this.hasMany(Build, 'repositories_id'); } + access_token() { return this.belongsTo(AccessToken, 'access_tokens_id'); } } export class Build extends Bookshelf.Model { diff --git a/src/api/db/repository.ts b/src/api/db/repository.ts index 7367d6b4b..778b96475 100644 --- a/src/api/db/repository.ts +++ b/src/api/db/repository.ts @@ -11,14 +11,30 @@ export function getRepository(id: number): Promise { } }, 'builds.repository', - 'builds.jobs' + 'builds.jobs.runs', + 'access_token.user' ] } as any) .then(repo => { if (!repo) { reject(repo); } else { - resolve(repo.toJSON()); + repo = repo.toJSON(); + repo.builds = repo.builds.map(build => { + build.jobs = build.jobs.map(job => { + if (job.runs.length > 0) { + job.end_time = job.runs[job.runs.length - 1].end_time; + job.start_time = job.runs[job.runs.length - 1].start_time; + job.status = job.runs[job.runs.length - 1].status; + } + + return job; + }); + + return build; + }); + + resolve(repo); } }).catch(err => reject(err)); }); @@ -26,7 +42,7 @@ export function getRepository(id: number): Promise { export function getRepositoryOnly(id: number): Promise { return new Promise((resolve, reject) => { - new Repository({ id: id }).fetch().then(repo => { + new Repository({ id: id }).fetch({ withRelated: ['access_token.user'] }).then(repo => { if (!repo) { reject(repo); } else { @@ -51,9 +67,13 @@ export function getRepositoryBadge(id: number): Promise { resolve('unknown'); } else { repo = repo.toJSON(); - let status = 'queued'; + let status = 'unknown'; + + if (repo.builds[0] && repo.builds[0].runs[0]) { + if (repo.builds[0].runs[0].job_runs.findIndex(run => run.status === 'queued') !== -1) { + status = 'queued'; + } - if (repo.builds[0] && repo.builds[0].runs[0]) { if (repo.builds[0].runs[0].job_runs.findIndex(run => run.status === 'failed') !== -1) { status = 'failing'; } @@ -107,8 +127,8 @@ export function getRepositories(userId: string, keyword: string): Promise export function getRepositoryId(owner: string, repository: string): Promise { return new Promise((resolve, reject) => { - new Repository().query(q => q.where('full_name', `${owner}/${repository}`)) - .fetch().then(repo => !repo ? reject() : resolve(repo.toJSON().id)); + new Repository().query(q => q.where('full_name', `${owner}/${repository}`)).fetch() + .then(repo => !repo ? reject() : resolve(repo.toJSON().id)); }); } @@ -124,6 +144,13 @@ export function addRepository(data: any): Promise { }); } +export function saveRepositorySettings(data: any): Promise { + return new Promise((resolve, reject) => { + new Repository({ id: data.id }).save(data, { method: 'update', require: false }) + .then(repo => !repo ? reject(repo) : resolve(repo.toJSON())); + }); +} + export function updateRepository(data: any): Promise { return new Promise((resolve, reject) => { new Repository().where({ github_id: data.github_id }) diff --git a/src/api/db/user.ts b/src/api/db/user.ts index 75fd7292b..b3fe550a8 100644 --- a/src/api/db/user.ts +++ b/src/api/db/user.ts @@ -3,13 +3,20 @@ import { generatePassword, comparePassword, generateJwt } from '../security'; export function getUser(id: number): Promise { return new Promise((resolve, reject) => { - new User({ id: id }).fetch().then(user => { - if (!user) { - reject(user); - } else { - resolve(user.toJSON()); - } - }); + new User({ id: id }).fetch({ withRelated: ['access_tokens'] }) + .then(user => { + if (!user) { + reject(user); + } else { + let result = user.toJSON(); + result.access_tokens = result.access_tokens.map(token => { + delete token.token; + return token; + }); + + resolve(result); + } + }); }); } diff --git a/src/api/server-routes.ts b/src/api/server-routes.ts index 2986eb112..6867de3f1 100644 --- a/src/api/server-routes.ts +++ b/src/api/server-routes.ts @@ -21,10 +21,12 @@ import { getRepositories, getRepository, getRepositoryBadge, - getRepositoryId + getRepositoryId, + saveRepositorySettings } from './db/repository'; import { getBuilds, getBuild } from './db/build'; import { getJob } from './db/job'; +import { insertAccessToken, getAccessTokens } from './db/access-token'; import { imageExists } from './docker'; export function webRoutes(): express.Router { @@ -120,6 +122,24 @@ export function userRoutes(): express.Router { .catch(err => res.status(400).json({ err: err })); }); + router.post('/add-token', (req: express.Request, res: express.Response) => { + insertAccessToken(req.body) + .then(() => res.status(200).json({ data: true })) + .catch(() => res.status(200).json({ data: false })); + }); + + return router; +} + +export function tokenRoutes(): express.Router { + const router = express.Router(); + + router.get('/', (req: express.Request, res: express.Response) => { + getAccessTokens() + .then(tokens => res.status(200).json({ data: tokens })) + .catch(() => res.status(200).json({ data: false })); + }); + return router; } @@ -144,6 +164,12 @@ export function repositoryRoutes(): express.Router { }).catch(err => res.status(200).json({ status: false })); }); + router.post('/save', (req: express.Request, res: express.Response) => { + saveRepositorySettings(req.body) + .then(() => res.status(200).json({ data: true })) + .catch(() => res.status(200).json({ data: false })); + }); + return router; } @@ -160,12 +186,12 @@ export function badgeRoutes(): express.Router { router.get('/:owner/:repository', (req: express.Request, res: express.Response) => { getRepositoryId(req.params.owner, req.params.repository) - .then(id => getRepositoryBadge(id)) - .then(status => { - res.writeHead(200, {'Content-Type': 'image/svg+xml'}); - res.write(generateBadgeHtml(status)); - res.end(); - }); + .then(id => getRepositoryBadge(id)) + .then(status => { + res.writeHead(200, {'Content-Type': 'image/svg+xml'}); + res.write(generateBadgeHtml(status)); + res.end(); + }); }); return router; diff --git a/src/api/server.ts b/src/api/server.ts index dd8fb7325..57d1fb10f 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -29,6 +29,7 @@ export class ExpressServer implements IExpressServer { app.use('/webhooks', webhooks); app.use('/api/setup', routes.setupRoutes()); app.use('/api/user', routes.userRoutes()); + app.use('/api/tokens', routes.tokenRoutes()); app.use('/api/repositories', routes.repositoryRoutes()); app.use('/api/builds', routes.buildRoutes()); app.use('/api/jobs', routes.jobRoutes()); diff --git a/src/api/utils.ts b/src/api/utils.ts index 4c52a9a65..36399ead1 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -131,12 +131,14 @@ export function generateBadgeHtml(status: string): string { background = '#ffd43b'; } else if (status === 'queued') { background = '#ffd43b'; + } else if (status === 'unknown') { + background = '#3A7EE1'; } else { background = '#39B54A'; } return ` - @@ -144,7 +146,7 @@ export function generateBadgeHtml(status: string): string { - + @@ -171,7 +173,7 @@ c-0.02-0.32,0.12-0.66-0.02-0.98c-0.75-0.04-1.5, c0.46-0.82,1.25-1.45,2.16-1.67c0.7-0.06,1.41-0.01,2.1-0.02c-0.01-1.6, 0-3.2-0.01-4.81C18.09,13.33,18.42,12.27,19.18,11.57z"/> - + build build ` + status + ` diff --git a/src/app/components/app-repository/app-repository.component.html b/src/app/components/app-repository/app-repository.component.html index 616ccb586..4ca1204eb 100644 --- a/src/app/components/app-repository/app-repository.component.html +++ b/src/app/components/app-repository/app-repository.component.html @@ -64,14 +64,23 @@

{{ repo?.full_name }}

-
+
-

Settings

-
-
-
- -
+
+

Settings

+
+
+ + +
+ +
+ +
+
+
diff --git a/src/app/components/app-repository/app-repository.component.ts b/src/app/components/app-repository/app-repository.component.ts index a526a8ecd..bee098152 100644 --- a/src/app/components/app-repository/app-repository.component.ts +++ b/src/app/components/app-repository/app-repository.component.ts @@ -5,6 +5,12 @@ import { ApiService } from '../../services/api.service'; import { ConfigService } from '../../services/config.service'; import { Subscription } from 'rxjs/Subscription'; import { format, distanceInWordsToNow } from 'date-fns'; +import 'rxjs/add/operator/delay'; + +export interface IRepoForm { + id: number; + access_tokens_id: any; +} @Component({ selector: 'app-repository', @@ -18,6 +24,9 @@ export class AppRepositoryComponent implements OnInit, OnDestroy { repo: any; url: string; statusBadge: string; + tokens: any[]; + saving: boolean; + form: IRepoForm; constructor( private route: ActivatedRoute, @@ -30,7 +39,7 @@ export class AppRepositoryComponent implements OnInit, OnDestroy { } ngOnInit() { - this.tab = 'settings'; + this.tab = 'builds'; this.url = this.config.url; this.route.params.subscribe(params => { @@ -40,6 +49,7 @@ export class AppRepositoryComponent implements OnInit, OnDestroy { } else { this.fetch(); this.fetchBadge(); + this.fetchTokens(); } }); @@ -94,6 +104,7 @@ export class AppRepositoryComponent implements OnInit, OnDestroy { fetch(): void { this.api.getRepository(this.id).subscribe(event => { this.repo = event; + this.form = { id: parseInt(this.id, 10), access_tokens_id: event.access_tokens_id }; this.loading = false; this.updateJobs(); setInterval(() => this.updateJobs(), 1000); @@ -108,6 +119,12 @@ export class AppRepositoryComponent implements OnInit, OnDestroy { }); } + fetchTokens(): void { + this.api.getAllTokens().subscribe(tokens => { + this.tokens = tokens; + }); + } + updateJobs(): void { let currentTime = new Date().getTime() - this.socketService.timeSyncDiff; @@ -155,4 +172,16 @@ export class AppRepositoryComponent implements OnInit, OnDestroy { gotoBuild(buildId: number) { this.router.navigate(['build', buildId]); } + + saveRepoSettings(e: MouseEvent): void { + this.saving = true; + + this.api.saveRepositorySettings(this.form) + .delay(1000) + .subscribe(saved => { + if (saved) { + this.saving = false; + } + }); + } } diff --git a/src/app/components/app-settings/app-settings.component.html b/src/app/components/app-settings/app-settings.component.html index 6968f7994..d02168806 100644 --- a/src/app/components/app-settings/app-settings.component.html +++ b/src/app/components/app-settings/app-settings.component.html @@ -21,7 +21,7 @@

My Settings

-
+
@@ -55,17 +55,22 @@

User Settings

Access Tokens

-
-
+
+ + No Access Tokens saved. +
+ +
+
-

GitHub Access Token

+

{{ token.description }}

- +
@@ -73,7 +78,7 @@

Access Tokens

-
+

Add New Access Token

@@ -83,7 +88,7 @@

Add New Access Token

- +
diff --git a/src/app/components/app-settings/app-settings.component.ts b/src/app/components/app-settings/app-settings.component.ts index 03a847d61..f16182a1c 100644 --- a/src/app/components/app-settings/app-settings.component.ts +++ b/src/app/components/app-settings/app-settings.component.ts @@ -20,6 +20,7 @@ export interface IUserPass { export interface IAccessToken { token: string; description: string; + users_id: number; } @Component({ @@ -34,28 +35,34 @@ export class AppSettingsComponent implements OnInit { userPass: IUserPass; avatarUrl: string; token: IAccessToken; + tokens: IAccessToken[]; - constructor(private api: ApiService, private auth: AuthService, private config: ConfigService) { } + constructor(private api: ApiService, private auth: AuthService, private config: ConfigService) { + this.loading = true; + } ngOnInit() { - const data: any = this.auth.getData(); - this.user = { - id: data.id, - email: data.email, - fullname: data.fullname, - admin: data.admin, - avatar: data.avatar - }; - - this.userPass = { - id: data.id, - password: '', - repeat_password: '' - }; + this.fetchUser(); + } - this.avatarUrl = this.config.url + data.avatar; + fetchUser(): void { + this.loading = true; + const user: any = this.auth.getData(); + this.api.getUser(user.id).subscribe(data => { + this.user = { + id: data.id, + email: data.email, + fullname: data.fullname, + admin: data.admin, + avatar: data.avatar + }; + this.userPass = { id: data.id, password: '', repeat_password: '' }; + this.avatarUrl = this.config.url + data.avatar; + this.token = { token: '', description: '', users_id: data.id }; + this.tokens = data.access_tokens; - this.token = { token: '', description: '' }; + this.loading = false; + }); } updateProfile(e: MouseEvent): void { @@ -81,4 +88,14 @@ export class AppSettingsComponent implements OnInit { } }); } + + addToken(e: MouseEvent): void { + e.preventDefault(); + + this.api.addToken(this.token).subscribe(event => { + if (event) { + this.fetchUser(); + } + }); + } } diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 285e40d96..e88d13be4 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -1,6 +1,7 @@ import { Injectable, Provider } from '@angular/core'; import { Http, Response, URLSearchParams, RequestOptions, Headers } from '@angular/http'; import { Observable } from 'rxjs/Observable'; +import { IAccessToken } from '../components/app-settings'; @Injectable() export class ApiService { @@ -45,6 +46,10 @@ export class ApiService { return this.post(`${this.url}/repositories/add`, data); } + saveRepositorySettings(data: any): Observable { + return this.post(`${this.url}/repositories/save`, data); + } + isAppReady(): Observable { return this.get(`${this.url}/setup/ready`); } @@ -65,6 +70,14 @@ export class ApiService { return this.post(`${this.url}/setup/db/init`, {}); } + getAllTokens(): Observable { + return this.get(`${this.url}/tokens`); + } + + addToken(data: IAccessToken): Observable { + return this.post(`${this.url}/user/add-token`, data); + } + getUsers(): Observable { return this.get(`${this.url}/user`); } diff --git a/src/app/styles/buttons.sass b/src/app/styles/buttons.sass index 9e870883a..babc17db0 100644 --- a/src/app/styles/buttons.sass +++ b/src/app/styles/buttons.sass @@ -76,6 +76,7 @@ &:focus, &:active box-shadow: none !important border-color: transparent !important + color: $white &.floated float: left diff --git a/src/app/styles/content.sass b/src/app/styles/content.sass index 494196bc9..9cae56330 100644 --- a/src/app/styles/content.sass +++ b/src/app/styles/content.sass @@ -56,14 +56,6 @@ font-size: 12px color: $color-secondary - - .columns - margin: 0 - - .column - margin: 0 - padding: 0 - .list-header background: $border color: $color-secondary @@ -229,6 +221,10 @@ display: flex justify-content: flex-end + .status-badge + width: 110px + height: 20px + .search-input-container display: inline-block position: relative diff --git a/src/app/styles/forms.sass b/src/app/styles/forms.sass index ea05dbdbe..01c6734cc 100644 --- a/src/app/styles/forms.sass +++ b/src/app/styles/forms.sass @@ -3,6 +3,7 @@ form border-radius: 4px border: 1px solid $border background: $background + padding: 5px 20px 10px 20px .form-field display: inline-block @@ -51,3 +52,6 @@ form border-color: $red-secondary color: $red + &.form-select + height: 40px + diff --git a/src/app/styles/repository.sass b/src/app/styles/repository.sass index 2e6fe0f69..616f89c79 100644 --- a/src/app/styles/repository.sass +++ b/src/app/styles/repository.sass @@ -1,8 +1,9 @@ .repository-details border: 1px solid $divider border-radius: 4px - background: $background-secondary + background: linear-gradient(to bottom, $background, $background-secondary) padding: 20px + margin: 10px 0 display: flex align-items: center diff --git a/src/app/styles/settings.sass b/src/app/styles/settings.sass index 859c80724..ccccfaeac 100644 --- a/src/app/styles/settings.sass +++ b/src/app/styles/settings.sass @@ -16,8 +16,15 @@ h2 display: block - margin-bottom: 10px + margin-bottom: 20px font-weight: 700 + padding: 0 + + .token-icon + display: block + width: 12px + margin: 0 10px 0 10px + float: left .tokens width: 100% @@ -27,21 +34,19 @@ .token-item display: block padding: 10px 5px - background: linear-gradient(to bottom, $background, $background-secondary) - border: 1px solid $divider + background: $red-secondary + border: 1px solid $white border-radius: 10px - .token-icon - display: block - width: 12px - margin: 0 auto - .column display: flex align-items: center .icon display: block + width: 20px + cursor: pointer + margin-top: 5px p font-size: 14px