diff --git a/web/app/common/constants.ts b/web/app/common/constants.ts index e90a5434..f2609c03 100644 --- a/web/app/common/constants.ts +++ b/web/app/common/constants.ts @@ -11,8 +11,8 @@ export enum LocalStorageKeys { AUTH_TOKEN = 'auth_token' } -export type FastlaneStatus = - 'failure'|'success'|'ci_problem'|'pending'|'missing_fastfile'|'installing_xcode'; +export type FastlaneStatus = 'failure'|'success'|'ci_problem'|'pending'| + 'missing_fastfile'|'installing_xcode'; export function fastlaneStatusToEnum(status: FastlaneStatus): BuildStatus { switch (status) { @@ -32,3 +32,9 @@ export function fastlaneStatusToEnum(status: FastlaneStatus): BuildStatus { throw new Error(`Unknown status type ${status}`); } } + +/** + * This is what is defined by GitHub to be their token length, but it can be + * modified. + */ +export const GITHUB_API_TOKEN_LENGTH = 40; diff --git a/web/app/onboard/onboard.component.html b/web/app/onboard/onboard.component.html index 98eaae88..43225deb 100644 --- a/web/app/onboard/onboard.component.html +++ b/web/app/onboard/onboard.component.html @@ -28,7 +28,53 @@
NOT WORKING - CURRENTLY IN DEVELOPMENT
- CI bot account set up +

fastlane.ci needs you to set up a CI bot account to update the build status, etc.

+
+
+ a + Create an account for your fastlane.ci bot on Github +
+
+
+
+ b + Create a personal access token for use with your bot on Github
+
+
Enable the following permissions + on Github:
+
+
+
+
+
+ c + Enter information for your fastlane.ci bot +
+
+
+ + +
+ + check_circle +
+
+ +
+ + {{botEmail}} +
+
+ + +
+
+
+
+ + +
+
Repository setup diff --git a/web/app/onboard/onboard.component.scss b/web/app/onboard/onboard.component.scss index 91622fa5..28cab103 100644 --- a/web/app/onboard/onboard.component.scss +++ b/web/app/onboard/onboard.component.scss @@ -24,8 +24,38 @@ } .mat-stepper-vertical { + .fci-substep { + font-size: 14px; + color: #4a4a4a; + .fci-substep-label { + margin-bottom: 24px; + .fci-substep-letter { + color: #9b9b9b; + border: 1px solid #9b9b9b; + border-radius: 50%; + font-size: 12px; + margin-right: 16px; + padding: 4px 8px; + } + } + .fci-substep-content { + padding-left: 40px; + } + } .fci-input-container { padding-bottom: 24px; + .fci-username { + font-size: 16px; + } + .fci-input-status { + display: inline-block; + padding-left: 8px; + .mat-spinner, + .mat-icon { + vertical-align: middle; + display: inline-block; + } + } } p { margin-top: 8px; @@ -33,4 +63,18 @@ font-size: 14px; color: #4a4a4a; } + button:first-child { + margin-right: 8px; + } +} + +.fci-github-token-scopes-image { + background: url(/assets/github_token_scopes.png); + background-repeat: no-repeat; + background-size: contain; + height: 214px; + width: 595px; + border: 1px solid rgba(0, 0, 0, 0.08); + margin-top: 16px; + margin-bottom: 32px; } diff --git a/web/app/onboard/onboard.component.spec.ts b/web/app/onboard/onboard.component.spec.ts index 5a46a980..21883aaf 100644 --- a/web/app/onboard/onboard.component.spec.ts +++ b/web/app/onboard/onboard.component.spec.ts @@ -1,31 +1,45 @@ import {DOCUMENT} from '@angular/common'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ReactiveFormsModule} from '@angular/forms'; -import {MatButtonModule, MatStepperModule} from '@angular/material'; +import {MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatStepperModule} from '@angular/material'; import {By} from '@angular/platform-browser'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {Subject} from 'rxjs/Subject'; + +import {UserDetails} from '../common/types'; +import {DataService} from '../services/data.service'; import {OnboardComponent} from './onboard.component'; -const FORM_CONTROL_IDS: string[] = ['encryptionKey']; +const FORM_CONTROL_IDS: string[] = ['encryptionKey', 'botToken', 'botPassword']; +const FORTY_CHAR_STRING: string = new Array(40 + 1).join('a'); +const THIRY_NINE_CHAR_STRING: string = new Array(39 + 1).join('a'); describe('OnboardComponent', () => { let fixture: ComponentFixture; - let document: jasmine.SpyObj; let component: OnboardComponent; + let dataService: jasmine.SpyObj>; + let userDetailsSubject: Subject; beforeEach(() => { - document = {location: {origin: 'fake-host', href: 'fake-href'}}; + userDetailsSubject = new Subject(); + dataService = { + getUserDetails: + jasmine.createSpy().and.returnValue(userDetailsSubject.asObservable()) + }; TestBed .configureTestingModule({ imports: [ MatStepperModule, BrowserAnimationsModule, MatButtonModule, - ReactiveFormsModule + ReactiveFormsModule, MatProgressSpinnerModule, MatIconModule ], declarations: [ OnboardComponent, ], + providers: [ + {provide: DataService, useValue: dataService}, + ] }) .compileComponents(); @@ -34,31 +48,127 @@ describe('OnboardComponent', () => { fixture.detectChanges(); }); - for (const control_id of FORM_CONTROL_IDS) { - it(`should have the ${control_id} control properly attached`, () => { - const controlEl: HTMLInputElement = - fixture.debugElement - .query(By.css(`input[formcontrolname="${control_id}"]`)) - .nativeElement; + describe('Unit tests', () => { + it('should not get user details when the bot token is 39 chars', () => { + component.form.patchValue({botToken: THIRY_NINE_CHAR_STRING}); + + expect(dataService.getUserDetails).not.toHaveBeenCalled(); + }); + + it('should get user details when the bot token is 40 chars', () => { + component.form.patchValue({botToken: FORTY_CHAR_STRING}); + + expect(dataService.getUserDetails) + .toHaveBeenCalledWith(FORTY_CHAR_STRING); + }); + + it('should clear email if the token is changed', () => { + component.botEmail = 'fake@email.com'; + component.form.patchValue({botToken: 'new value'}); + + expect(component.botEmail).toBeUndefined(); + }); + + it('should set email from user details request', () => { + component.form.patchValue({botToken: FORTY_CHAR_STRING}); + userDetailsSubject.next({github: {email: 'best@gmail.com'}}); + + expect(component.botEmail).toBe('best@gmail.com'); + }); + + it('should set isFetchingBotEmail to false after getting user details', + () => { + component.form.patchValue({botToken: FORTY_CHAR_STRING}); + expect(component.isFetchingBotEmail).toBe(true); + + userDetailsSubject.next({github: {email: 'best@gmail.com'}}); + + expect(component.isFetchingBotEmail).toBe(false); + }); + }); - controlEl.value = '10'; - controlEl.dispatchEvent(new Event('input')); + describe('Shallow tests', () => { + let tokenInputEl: HTMLInputElement; + + beforeEach(() => { + component.botEmail = 'fake@email.com'; fixture.detectChanges(); - expect(component.form.get(control_id).value).toBe('10'); + tokenInputEl = fixture.debugElement + .query(By.css('input[formcontrolname="botToken"]')) + .nativeElement; + }); + + for (const control_id of FORM_CONTROL_IDS) { + it(`should have the ${control_id} control properly attached`, () => { + const controlEl: HTMLInputElement = + fixture.debugElement + .query(By.css(`input[formcontrolname="${control_id}"]`)) + .nativeElement; + + controlEl.value = '10'; + controlEl.dispatchEvent(new Event('input')); + fixture.detectChanges(); - component.form.patchValue({[control_id]: '12'}); + expect(component.form.get(control_id).value).toBe('10'); + + component.form.patchValue({[control_id]: '12'}); + fixture.detectChanges(); + + expect(controlEl.value).toBe('12'); + }); + } + + it('should show success check mark if bot email exists', () => { + expect(component.botEmail).toBeDefined(); + expect(fixture.debugElement + .queryAll(By.css('.fci-input-status .fci-success-icon')) + .length) + .toBe(1); + }); + + it('should hide bot email and password if token changes', () => { + expect(fixture.debugElement + .queryAll(By.css('input[formcontrolname="botPassword"]')) + .length) + .toBe(1); + expect(fixture.debugElement.query(By.css('.fci-username')) + .nativeElement.textContent) + .toBe('fake@email.com'); + + tokenInputEl.value = FORTY_CHAR_STRING; + tokenInputEl.dispatchEvent(new Event('input')); fixture.detectChanges(); - expect(controlEl.value).toBe('12'); + expect(fixture.debugElement + .queryAll(By.css('input[formcontrolname="botPassword"]')) + .length) + .toBe(0); + expect(fixture.debugElement.queryAll(By.css('.fci-username')).length) + .toBe(0); }); - } - it('should redirect to old onboarding when button is clicked', () => { - spyOn(component, 'goToOldOnboarding'); - fixture.debugElement.query(By.css('.fci-onboard-welcome button')) - .nativeElement.click(); + it('should show spinner when looking for bot email', () => { + expect(fixture.debugElement + .queryAll(By.css('.fci-input-status .mat-spinner')) + .length) + .toBe(0); + tokenInputEl.value = FORTY_CHAR_STRING; + tokenInputEl.dispatchEvent(new Event('input')); + fixture.detectChanges(); - expect(component.goToOldOnboarding).toHaveBeenCalled(); + expect(fixture.debugElement + .queryAll(By.css('.fci-input-status .mat-spinner')) + .length) + .toBe(1); + }); + + it('should redirect to old onboarding when button is clicked', () => { + spyOn(component, 'goToOldOnboarding'); + fixture.debugElement.query(By.css('.fci-onboard-welcome button')) + .nativeElement.click(); + + expect(component.goToOldOnboarding).toHaveBeenCalled(); + }); }); }); diff --git a/web/app/onboard/onboard.component.ts b/web/app/onboard/onboard.component.ts index 99a967a9..38bb9790 100644 --- a/web/app/onboard/onboard.component.ts +++ b/web/app/onboard/onboard.component.ts @@ -2,10 +2,15 @@ import {DOCUMENT} from '@angular/common'; import {Component, Inject} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {GITHUB_API_TOKEN_LENGTH} from '../common/constants'; +import {DataService} from '../services/data.service'; + function buildProjectForm(fb: FormBuilder): FormGroup { return fb.group({ 'encryptionKey': ['', Validators.required], + 'botToken': ['', Validators.required], + 'botPassword': ['', Validators.required], }); } @@ -16,12 +21,27 @@ function buildProjectForm(fb: FormBuilder): FormGroup { }) export class OnboardComponent { readonly form: FormGroup; + botEmail: string; + isFetchingBotEmail = false; constructor( @Inject(DOCUMENT) private readonly document: any, + private readonly dataService: DataService, fb: FormBuilder, ) { this.form = buildProjectForm(fb); + + this.form.get('botToken').valueChanges.subscribe((token) => { + delete this.botEmail; + if (token.length === GITHUB_API_TOKEN_LENGTH) { + this.isFetchingBotEmail = true; + + this.dataService.getUserDetails(token).subscribe((details) => { + this.botEmail = details.github.email; + this.isFetchingBotEmail = false; + }); + } + }); } goToOldOnboarding() { diff --git a/web/app/onboard/onboard.module.ts b/web/app/onboard/onboard.module.ts index 6c2e86c8..143a868e 100644 --- a/web/app/onboard/onboard.module.ts +++ b/web/app/onboard/onboard.module.ts @@ -1,6 +1,7 @@ +import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {ReactiveFormsModule} from '@angular/forms'; -import {MatButtonModule, MatStepperModule} from '@angular/material'; +import {MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatStepperModule} from '@angular/material'; import {OnboardComponent} from './onboard.component'; @@ -14,10 +15,10 @@ import {OnboardComponent} from './onboard.component'; ], imports: [ /** Angular Library Imports */ - ReactiveFormsModule, + ReactiveFormsModule, CommonModule, /** Internal Imports */ /** Angular Material Imports */ - MatButtonModule, MatStepperModule, + MatButtonModule, MatStepperModule, MatProgressSpinnerModule, MatIconModule /** Third-Party Module Imports */ ], providers: [], diff --git a/web/assets/github_token_scopes.png b/web/assets/github_token_scopes.png new file mode 100644 index 00000000..b89af870 Binary files /dev/null and b/web/assets/github_token_scopes.png differ diff --git a/web/global/theme.scss b/web/global/theme.scss index 84fc9e51..36ce4c0e 100644 --- a/web/global/theme.scss +++ b/web/global/theme.scss @@ -20,3 +20,7 @@ body, display: block; position: relative; } + +.fci-success-icon { + color: #00897b; +}