From 7a352ddfbcdd304ce7f2cb704521f45474833926 Mon Sep 17 00:00:00 2001 From: Alec Merdler Date: Wed, 5 Apr 2017 14:14:08 -0700 Subject: [PATCH] Use ng-metadata as a Backport of Angular 2+ API (#2486) * starting UtilService refactor * pre find-replace angular.module('quay') => angular.module('QuayModule') * successfully switched to ng-metadata for backported Angular2 API * working with parent component reference in child * fixing @Output to use EventEmitter * fixed @Output events for custom git trigger * more fixes * refactored QuayPages module for backwards-compatibility * reinitialized test.db * use minified libraries * replaced references for angular-ts-decorators * fixed ng-show --- package.json | 4 +- .../app-public-view.component.ts | 19 +- .../ui/channel-icon/channel-icon.component.ts | 10 +- .../context-path-select.component.spec.ts | 10 +- .../context-path-select.component.ts | 8 +- .../ui/cor-table/cor-table-col.component.ts | 17 +- .../ui/cor-table/cor-table.component.ts | 16 +- .../dockerfile-path-select.component.spec.ts | 6 +- .../dockerfile-path-select.component.ts | 8 +- .../linear-workflow-section.component.spec.ts | 18 +- .../linear-workflow-section.component.ts | 20 +- .../linear-workflow.component.spec.ts | 22 +- .../linear-workflow.component.ts | 14 +- .../manage-trigger-custom-git.component.html | 2 +- ...anage-trigger-custom-git.component.spec.ts | 2 +- .../manage-trigger-custom-git.component.ts | 10 +- .../manage-trigger-githost.component.html | 2 +- .../manage-trigger-githost.component.ts | 22 +- .../regex-match-view.component.ts | 10 +- .../visibility-indicator.component.ts | 7 +- static/js/main.ts | 15 + static/js/pages/trigger-setup.js | 8 +- static/js/quay-config.module.ts | 139 ++++----- static/js/quay-pages.module.ts | 25 +- static/js/quay-routes.module.ts | 275 +++++++++--------- static/js/quay-run.module.ts | 179 ------------ static/js/quay-run.ts | 133 +++++++++ static/js/quay.module.ts | 30 +- .../js/services/avatar/avatar.service.impl.ts | 5 +- .../js/services/build/build.service.impl.ts | 2 +- .../datafile/datafile.service.impl.ts | 4 +- .../dockerfile/dockerfile.service.impl.ts | 8 +- static/js/services/page/page.service.impl.ts | 2 +- .../route-builder.service.impl.ts | 6 +- .../route-builder.service.spec.ts | 1 + .../services/util/util.service.impl.spec.ts | 40 +++ static/js/services/util/util.service.impl.ts | 39 +++ static/js/services/util/util.service.ts | 19 ++ .../js/services/view-array/view-array.impl.ts | 11 +- static/partials/trigger-setup.html | 4 +- tsconfig.json | 1 + webpack.config.js | 2 +- yarn.lock | 18 ++ 43 files changed, 642 insertions(+), 551 deletions(-) create mode 100644 static/js/main.ts delete mode 100644 static/js/quay-run.module.ts create mode 100644 static/js/quay-run.ts create mode 100644 static/js/services/util/util.service.impl.spec.ts create mode 100644 static/js/services/util/util.service.impl.ts create mode 100644 static/js/services/util/util.service.ts diff --git a/package.json b/package.json index 8d21d6401..497a026c6 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "d3": "^3.3.3", "eonasdan-bootstrap-datetimepicker": "^4.17.43", "jquery": "1.12.4", + "ng-metadata": "^4.0.1", "raven-js": "^3.1.0", "react": "^15.3.2", "react-dom": "^15.3.2", "restangular": "^1.2.0", + "rxjs": "^5.0.1", "underscore": "^1.5.2" }, "devDependencies": { @@ -39,7 +41,7 @@ "@types/angular-mocks": "^1.5.8", "@types/angular-route": "^1.3.3", "@types/angular-sanitize": "^1.3.4", - "@types/es6-shim": "^0.31.32", + "@types/core-js": "^0.9.39", "@types/jasmine": "^2.5.41", "@types/jquery": "^2.0.40", "@types/react": "0.14.39", diff --git a/static/js/directives/ui/app-public-view/app-public-view.component.ts b/static/js/directives/ui/app-public-view/app-public-view.component.ts index fca444d94..0d90c188f 100644 --- a/static/js/directives/ui/app-public-view/app-public-view.component.ts +++ b/static/js/directives/ui/app-public-view/app-public-view.component.ts @@ -1,19 +1,20 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component, Inject } from 'ng-metadata/core'; + /** * A component that displays the public information associated with an application repository. */ @Component({ - selector: 'appPublicView', + selector: 'app-public-view', templateUrl: '/static/js/directives/ui/app-public-view/app-public-view.component.html' }) -export class AppPublicViewComponent implements ng.IComponentController { +export class AppPublicViewComponent { @Input('<') public repository: any; private currentTab: string = 'description'; private settingsShown: number = 0; - constructor(private Config: any) { - this.updateDescription = this.updateDescription.bind(this); + constructor(@Inject('Config') private Config: any) { + this.updateDescription = this.updateDescription.bind(this); } private updateDescription(content: string) { @@ -22,9 +23,9 @@ export class AppPublicViewComponent implements ng.IComponentController { } public showTab(tab: string): void { - this.currentTab = tab; - if (tab == 'settings') { - this.settingsShown++; - } + this.currentTab = tab; + if (tab == 'settings') { + this.settingsShown++; + } } } diff --git a/static/js/directives/ui/channel-icon/channel-icon.component.ts b/static/js/directives/ui/channel-icon/channel-icon.component.ts index eebf69e33..6474238c8 100644 --- a/static/js/directives/ui/channel-icon/channel-icon.component.ts +++ b/static/js/directives/ui/channel-icon/channel-icon.component.ts @@ -1,18 +1,18 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component, Inject } from 'ng-metadata/core'; + /** * A component that displays the icon of a channel. */ @Component({ - selector: 'channelIcon', + selector: 'channel-icon', templateUrl: '/static/js/directives/ui/channel-icon/channel-icon.component.html', }) -export class ChannelIconComponent implements ng.IComponentController { +export class ChannelIconComponent { @Input('<') public name: string; - private colors: any; - constructor(Config: any, private md5: any) { + constructor(@Inject('Config') Config: any, @Inject('md5') private md5: any) { this.colors = Config['CHANNEL_COLORS']; } diff --git a/static/js/directives/ui/context-path-select/context-path-select.component.spec.ts b/static/js/directives/ui/context-path-select/context-path-select.component.spec.ts index 033ce4945..ebd2a71f6 100644 --- a/static/js/directives/ui/context-path-select/context-path-select.component.spec.ts +++ b/static/js/directives/ui/context-path-select/context-path-select.component.spec.ts @@ -17,23 +17,23 @@ describe("ContextPathSelectComponent", () => { component.contexts = contexts; }); - describe("$onChanges", () => { + describe("ngOnChanges", () => { it("sets valid context flag to true if current context is valid", () => { - component.$onChanges({}); + component.ngOnChanges({}); expect(component.isValidContext).toBe(true); }); it("sets valid context flag to false if current context is invalid", () => { component.currentContext = "asdfdsf"; - component.$onChanges({}); + component.ngOnChanges({}); expect(component.isValidContext).toBe(false); }); }); - describe("setcontext", () => { + describe("setContext", () => { var newContext: string; beforeEach(() => { @@ -59,7 +59,7 @@ describe("ContextPathSelectComponent", () => { }); }); - describe("setCurrentcontext", () => { + describe("setSelectedContext", () => { var context: string; beforeEach(() => { diff --git a/static/js/directives/ui/context-path-select/context-path-select.component.ts b/static/js/directives/ui/context-path-select/context-path-select.component.ts index 4d70992f6..7f7fb5297 100644 --- a/static/js/directives/ui/context-path-select/context-path-select.component.ts +++ b/static/js/directives/ui/context-path-select/context-path-select.component.ts @@ -1,14 +1,14 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component, OnChanges, SimpleChanges } from 'ng-metadata/core'; /** * A component that allows the user to select the location of the Context in their source code repository. */ @Component({ - selector: 'contextPathSelect', + selector: 'context-path-select', templateUrl: '/static/js/directives/ui/context-path-select/context-path-select.component.html' }) -export class ContextPathSelectComponent implements ng.IComponentController { +export class ContextPathSelectComponent implements OnChanges { // FIXME: Use one-way data binding @Input('=') public currentContext: string; @@ -17,7 +17,7 @@ export class ContextPathSelectComponent implements ng.IComponentController { private isUnknownContext: boolean = true; private selectedContext: string | null = null; - public $onChanges(changes: ng.IOnChangesObject): void { + public ngOnChanges(changes: SimpleChanges): void { this.isValidContext = this.checkContext(this.currentContext, this.contexts); } diff --git a/static/js/directives/ui/cor-table/cor-table-col.component.ts b/static/js/directives/ui/cor-table/cor-table-col.component.ts index 6428b7cbb..e9505c237 100644 --- a/static/js/directives/ui/cor-table/cor-table-col.component.ts +++ b/static/js/directives/ui/cor-table/cor-table-col.component.ts @@ -1,28 +1,27 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component, OnInit, Inject, Host } from 'ng-metadata/core'; import { CorTableComponent } from './cor-table.component'; + /** * Defines a column (optionally sortable) in the table. */ @Component({ - selector: 'corTableCol', + selector: 'cor-table-col', template: '', - require: { - parent: '^^corTable' - }, }) -export class CorTableColumn implements ng.IComponentController { +export class CorTableColumn implements OnInit { @Input('@') public title: string; @Input('@') public templateurl: string; - @Input('@') public datafield: string; @Input('@') public sortfield: string; @Input('@') public selected: string; @Input('@') public dataKind: string; - private parent: CorTableComponent; + constructor(@Host() @Inject(CorTableComponent) private parent: CorTableComponent) { - public $onInit(): void { + } + + public ngOnInit(): void { this.parent.addColumn(this); } diff --git a/static/js/directives/ui/cor-table/cor-table.component.ts b/static/js/directives/ui/cor-table/cor-table.component.ts index cc0e23c38..60982f6e7 100644 --- a/static/js/directives/ui/cor-table/cor-table.component.ts +++ b/static/js/directives/ui/cor-table/cor-table.component.ts @@ -1,26 +1,28 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component, OnChanges, SimpleChanges, Inject } from 'ng-metadata/core'; import { CorTableColumn } from './cor-table-col.component'; + /** * A component that displays a table of information, with optional filtering and automatic sorting. */ @Component({ - selector: 'corTable', + selector: 'cor-table', templateUrl: '/static/js/directives/ui/cor-table/cor-table.component.html', - transclude: true, + legacy: { + transclude: true + } }) -export class CorTableComponent implements ng.IComponentController { +export class CorTableComponent implements OnChanges { @Input('<') public tableData: any[]; @Input('@') public tableItemTitle: string; @Input('<') public filterFields: string[]; @Input('@') public compact: string; @Input('<') public maxDisplayCount: number; - private columns: CorTableColumn[]; private orderedData: any; private options: any; - constructor(private TableService: any) { + constructor(@Inject('TableService') private TableService: any) { this.columns = []; this.options = { 'filter': '', @@ -30,7 +32,7 @@ export class CorTableComponent implements ng.IComponentController { }; } - public $onChanges(changes: ng.IOnChangesObject): void { + public ngOnChanges(changes: SimpleChanges): void { if (changes['tableData'] !== undefined) { this.refreshOrder(); } diff --git a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts index 431252faa..f1210037d 100644 --- a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts +++ b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts @@ -20,17 +20,17 @@ describe("DockerfilePathSelectComponent", () => { component.supportsFullListing = supportsFullListing; }); - describe("$onChanges", () => { + describe("ngOnChanges", () => { it("sets valid path flag to true if current path is valid", () => { - component.$onChanges({}); + component.ngOnChanges({}); expect(component.isValidPath).toBe(true); }); it("sets valid path flag to false if current path is invalid", () => { component.currentPath = "asdfdsf"; - component.$onChanges({}); + component.ngOnChanges({}); expect(component.isValidPath).toBe(false); }); diff --git a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts index be0109ef4..ea012ef13 100644 --- a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts +++ b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts @@ -1,14 +1,14 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component, OnChanges, SimpleChanges } from 'ng-metadata/core'; /** * A component that allows the user to select the location of the Dockerfile in their source code repository. */ @Component({ - selector: 'dockerfilePathSelect', + selector: 'dockerfile-path-select', templateUrl: '/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.html' }) -export class DockerfilePathSelectComponent implements ng.IComponentController { +export class DockerfilePathSelectComponent implements OnChanges { // FIXME: Use one-way data binding @Input('=') public currentPath: string; @@ -18,7 +18,7 @@ export class DockerfilePathSelectComponent implements ng.IComponentController { private isUnknownPath: boolean = true; private selectedPath: string | null = null; - public $onChanges(changes: ng.IOnChangesObject): void { + public ngOnChanges(changes: SimpleChanges): void { this.isValidPath = this.checkPath(this.currentPath, this.paths, this.supportsFullListing); } diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts index d41ecf194..f2a65b337 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts @@ -1,5 +1,6 @@ import { LinearWorkflowSectionComponent } from './linear-workflow-section.component'; import { LinearWorkflowComponent } from './linear-workflow.component'; +import { SimpleChanges } from 'ng-metadata/core'; import Spy = jasmine.Spy; @@ -8,24 +9,23 @@ describe("LinearWorkflowSectionComponent", () => { var parentMock: LinearWorkflowComponent; beforeEach(() => { - component = new LinearWorkflowSectionComponent(); parentMock = new LinearWorkflowComponent(); - component.parent = parentMock; + component = new LinearWorkflowSectionComponent(parentMock); }); - describe("$onInit", () => { + describe("ngOnInit", () => { it("calls parent component to add itself as a section", () => { var addSectionSpy: Spy = spyOn(parentMock, "addSection").and.returnValue(null); - component.$onInit(); + component.ngOnInit(); expect(addSectionSpy.calls.argsFor(0)[0]).toBe(component); }); }); - describe("$onChanges", () => { + describe("ngOnChanges", () => { var onSectionInvalidSpy: Spy; - var changesObj: ng.IOnChangesObject; + var changesObj: SimpleChanges; beforeEach(() => { onSectionInvalidSpy = spyOn(parentMock, "onSectionInvalid").and.returnValue(null); @@ -39,20 +39,20 @@ describe("LinearWorkflowSectionComponent", () => { }); it("does nothing if 'sectionValid' input not changed", () => { - component.$onChanges({}); + component.ngOnChanges({}); expect(onSectionInvalidSpy).not.toHaveBeenCalled(); }); it("does nothing if 'sectionValid' input is true", () => { - component.$onChanges(changesObj); + component.ngOnChanges(changesObj); expect(onSectionInvalidSpy).not.toHaveBeenCalled(); }); it("calls parent method to inform that section is invalid if 'sectionValid' input changed to false", () => { changesObj['sectionValid'].currentValue = false; - component.$onChanges(changesObj); + component.ngOnChanges(changesObj); expect(onSectionInvalidSpy.calls.argsFor(0)[0]).toEqual(component.sectionId); }); diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts index 81e2c04ba..8896f640c 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts @@ -1,4 +1,4 @@ -import { Component, Output, Input } from 'angular-ts-decorators'; +import { Component, Input, Inject, Host, OnChanges, OnInit, SimpleChanges } from 'ng-metadata/core'; import { LinearWorkflowComponent } from './linear-workflow.component'; @@ -6,27 +6,29 @@ import { LinearWorkflowComponent } from './linear-workflow.component'; * A component which displays a single section in a linear workflow. */ @Component({ - selector: 'linearWorkflowSection', + selector: 'linear-workflow-section', templateUrl: '/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html', - transclude: true, - require: { - parent: '^^linearWorkflow' + legacy: { + transclude: true } }) -export class LinearWorkflowSectionComponent implements ng.IComponentController { +export class LinearWorkflowSectionComponent implements OnChanges, OnInit { @Input('@') public sectionId: string; @Input('@') public sectionTitle: string; @Input() public sectionValid: boolean = false; public sectionVisible: boolean = false; public isCurrentSection: boolean = false; - public parent: LinearWorkflowComponent; - public $onInit(): void { + constructor(@Host() @Inject(LinearWorkflowComponent) private parent: LinearWorkflowComponent) { + + } + + public ngOnInit(): void { this.parent.addSection(this); } - public $onChanges(changes: ng.IOnChangesObject): void { + public ngOnChanges(changes: SimpleChanges): void { if (changes['sectionValid'] !== undefined && !changes['sectionValid'].currentValue) { this.parent.onSectionInvalid(this.sectionId); } diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts index 9f479b51b..43bb5e3e3 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts @@ -14,11 +14,11 @@ describe("LinearWorkflowComponent", () => { var newSection: LinearWorkflowSectionComponent; beforeEach(() => { - newSection = new LinearWorkflowSectionComponent; + newSection = new LinearWorkflowSectionComponent(component); }); it("does not set 'sectionVisible' or 'isCurrentSection' of given section if not the first section added", () => { - component.addSection(new LinearWorkflowSectionComponent); + component.addSection(new LinearWorkflowSectionComponent(component)); component.addSection(newSection); expect(newSection.sectionVisible).toBe(false); @@ -42,8 +42,8 @@ describe("LinearWorkflowComponent", () => { var currentSection: LinearWorkflowSectionComponent; beforeEach(() => { - component.onWorkflowComplete = jasmine.createSpy("onWorkflowComplete").and.returnValue(null); - currentSection = new LinearWorkflowSectionComponent; + component.onWorkflowComplete = jasmine.createSpyObj("onWorkflowCompleteSpy", ['emit']); + currentSection = new LinearWorkflowSectionComponent(component); currentSection.sectionValid = true; component.addSection(currentSection); }); @@ -52,18 +52,18 @@ describe("LinearWorkflowComponent", () => { currentSection.sectionValid = false; component.onNextSection(); - expect(component.onWorkflowComplete).not.toHaveBeenCalled(); + expect(component.onWorkflowComplete.emit).not.toHaveBeenCalled(); expect(currentSection.isCurrentSection).toBe(true); }); it("calls workflow completed output callback if current section is the last section and is valid", () => { component.onNextSection(); - expect(component.onWorkflowComplete).toHaveBeenCalled(); + expect(component.onWorkflowComplete.emit).toHaveBeenCalled(); }); it("sets the current section to the next section if there are remaining sections and current section valid", () => { - var nextSection: LinearWorkflowSectionComponent = new LinearWorkflowSectionComponent(); + var nextSection: LinearWorkflowSectionComponent = new LinearWorkflowSectionComponent(component); component.addSection(nextSection); component.onNextSection(); @@ -78,15 +78,15 @@ describe("LinearWorkflowComponent", () => { var sections: LinearWorkflowSectionComponent[]; beforeEach(() => { - invalidSection = new LinearWorkflowSectionComponent(); + invalidSection = new LinearWorkflowSectionComponent(component); invalidSection.sectionId = "Git Repository"; invalidSection.sectionValid = false; component.addSection(invalidSection); sections = [ - new LinearWorkflowSectionComponent(), - new LinearWorkflowSectionComponent(), - new LinearWorkflowSectionComponent(), + new LinearWorkflowSectionComponent(component), + new LinearWorkflowSectionComponent(component), + new LinearWorkflowSectionComponent(component), ]; sections.forEach((section) => { section.sectionVisible = false; diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts index 960289c15..a3c93d46d 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts @@ -1,4 +1,4 @@ -import { Component, Output, Input } from 'angular-ts-decorators'; +import { Component, Output, Input, EventEmitter } from 'ng-metadata/core'; import { LinearWorkflowSectionComponent } from './linear-workflow-section.component'; @@ -7,14 +7,16 @@ import { LinearWorkflowSectionComponent } from './linear-workflow-section.compon * step is made visible. */ @Component({ - selector: 'linearWorkflow', + selector: 'linear-workflow', templateUrl: '/static/js/directives/ui/linear-workflow/linear-workflow.component.html', - transclude: true + legacy: { + transclude: true + } }) -export class LinearWorkflowComponent implements ng.IComponentController { +export class LinearWorkflowComponent { @Input('@') public doneTitle: string; - @Output() public onWorkflowComplete: (event: any) => void; + @Output() public onWorkflowComplete: EventEmitter = new EventEmitter(); private sections: SectionInfo[] = []; private currentSection: SectionInfo; @@ -33,7 +35,7 @@ export class LinearWorkflowComponent implements ng.IComponentController { public onNextSection(): void { if (this.currentSection.component.sectionValid && this.currentSection.index + 1 >= this.sections.length) { - this.onWorkflowComplete({}); + this.onWorkflowComplete.emit({}); } else if (this.currentSection.component.sectionValid && this.currentSection.index + 1 < this.sections.length) { this.currentSection.component.isCurrentSection = false; diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html index 5c554e73c..8100dfc57 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html @@ -1,7 +1,7 @@
+ (on-workflow-complete)="$ctrl.activateTrigger.emit({config: $ctrl.config})"> { component = new ManageTriggerCustomGitComponent(); }); - describe("$onChanges", () => { + describe("ngOnChanges", () => { }); }); diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts index c4e88f7cf..de13ab20c 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts @@ -1,22 +1,22 @@ -import { Input, Output, Component } from 'angular-ts-decorators'; +import { Input, Output, Component, EventEmitter, OnChanges, SimpleChanges } from 'ng-metadata/core'; /** * A component that lets the user set up a build trigger for a custom Git repository. */ @Component({ - selector: 'manageTriggerCustomGit', + selector: 'manage-trigger-custom-git', templateUrl: '/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html' }) -export class ManageTriggerCustomGitComponent implements ng.IComponentController { +export class ManageTriggerCustomGitComponent implements OnChanges { // FIXME: Use one-way data binding @Input('=') public trigger: {config: any}; - @Output() public activateTrigger: (trigger: {config: any}) => void; + @Output() public activateTrigger: EventEmitter<{config: any, pull_robot?: any}> = new EventEmitter(); private config: any = {}; private currentState: any | null; - public $onChanges(changes: ng.IOnChangesObject): void { + public ngOnChanges(changes: SimpleChanges): void { if (changes['trigger'] !== undefined) { this.config = Object.assign({}, changes['trigger'].currentValue.config); } diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html index e81963bab..4c6228f8d 100644 --- a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html @@ -1,7 +1,7 @@
+ (on-workflow-complete)="$ctrl.createTrigger($event)"> void; + @Output() public activateTrigger: EventEmitter<{config: any, pull_robot?: any}> = new EventEmitter(); private config: any; private local: any = { namespaceOptions: { @@ -44,11 +44,11 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { private namespaceTitle: string; private namespace: any; - constructor(private ApiService: any, - private TableService: any, - private TriggerService: any, - private RolesService: any, - private $scope: ng.IScope) { + constructor(@Inject('ApiService') private ApiService: any, + @Inject('TableService') private TableService: any, + @Inject('TriggerService') private TriggerService: any, + @Inject('RolesService') private RolesService: any, + @Inject('$scope') private $scope: ng.IScope) { // FIXME: Here binding methods to class context in order to pass them as arguments to $scope.$watch this.buildOrderedNamespaces = this.buildOrderedNamespaces.bind(this); this.loadNamespaces = this.loadNamespaces.bind(this); @@ -60,7 +60,7 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { this.checkDockerfilePath = this.checkDockerfilePath.bind(this); } - public $onInit(): void { + public ngOnInit(): void { // TODO: Replace $scope.$watch with @Output methods for child component mutations or $onChanges for parent mutations this.$scope.$watch(() => this.trigger, this.initialSetup.bind(this)); this.$scope.$watch(() => this.repository, this.initialSetup.bind(this)); @@ -134,7 +134,7 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { } var activate = () => { - this.activateTrigger({'config': config, 'pull_robot': this.local.robotAccount}); + this.activateTrigger.emit({config: config, pull_robot: this.local.robotAccount}); }; if (this.local.robotAccount && this.local.triggerAnalysis.status == 'requiresrobot') { diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts index 256a2d99e..e560a4936 100644 --- a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts @@ -1,4 +1,4 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component } from 'ng-metadata/core'; /** @@ -6,19 +6,15 @@ import { Input, Component } from 'angular-ts-decorators'; * items. */ @Component({ - selector: 'regexMatchView', + selector: 'regex-match-view', templateUrl: '/static/js/directives/ui/regex-match-view/regex-match-view.component.html' }) -export class RegexMatchViewComponent implements ng.IComponentController { +export class RegexMatchViewComponent { // FIXME: Use one-way data binding @Input('=') private regex: string; @Input('=') private items: any[]; - constructor() { - - } - public filterMatches(regexstr: string, items: ({value: string})[], shouldMatch: boolean): ({value: string})[] | null { regexstr = regexstr || '.+'; diff --git a/static/js/directives/ui/visibility-indicator/visibility-indicator.component.ts b/static/js/directives/ui/visibility-indicator/visibility-indicator.component.ts index e4bf5a3c5..b5fb32940 100644 --- a/static/js/directives/ui/visibility-indicator/visibility-indicator.component.ts +++ b/static/js/directives/ui/visibility-indicator/visibility-indicator.component.ts @@ -1,14 +1,15 @@ -import { Input, Component } from 'angular-ts-decorators'; +import { Input, Component } from 'ng-metadata/core'; + /** * A component that displays a box with "Public" or "Private", depending on the visibility * of the repository. */ @Component({ - selector: 'visibilityIndicator', + selector: 'visibility-indicator', templateUrl: '/static/js/directives/ui/visibility-indicator/visibility-indicator.component.html' }) -export class VisibilityIndicatorComponent implements ng.IComponentController { +export class VisibilityIndicatorComponent { @Input('<') public repository: any; constructor() { diff --git a/static/js/main.ts b/static/js/main.ts new file mode 100644 index 000000000..06a3f74f3 --- /dev/null +++ b/static/js/main.ts @@ -0,0 +1,15 @@ +import 'core-js'; +import { bundle } from 'ng-metadata/core'; +import { QuayModule } from './quay.module'; +import { provideRun } from './quay-run'; +import * as angular from 'angular'; + + +/** + * Register ng-metadata module as a traditional AngularJS module on the global namespace for non-TypeScript components. + * TODO: Needed for non-TypeScript components/services to register themselves. Remove once they are migrated. + * See https://hotell.gitbooks.io/ng-metadata/content/docs/recipes/bootstrap.html + */ +const ng1QuayModule: string = bundle(QuayModule, []).name; +angular.module('quay', [ng1QuayModule]) + .run(provideRun); diff --git a/static/js/pages/trigger-setup.js b/static/js/pages/trigger-setup.js index 84c2ae1bc..c88373850 100644 --- a/static/js/pages/trigger-setup.js +++ b/static/js/pages/trigger-setup.js @@ -41,7 +41,7 @@ $scope.state = 'managing'; - $scope.activateTrigger = function(config, pull_robot) { + $scope.activateTrigger = function(event) { $scope.state = 'activating'; var params = { 'repository': namespace + '/' + name, @@ -49,11 +49,11 @@ }; var data = { - 'config': config + 'config': event.config }; - if (pull_robot) { - data['pull_robot'] = pull_robot['name']; + if (event.pull_robot) { + data['pull_robot'] = event.pull_robot['name']; } var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) { diff --git a/static/js/quay-config.module.ts b/static/js/quay-config.module.ts index 7f6c8cca1..6e7674a4d 100644 --- a/static/js/quay-config.module.ts +++ b/static/js/quay-config.module.ts @@ -1,11 +1,10 @@ -import { NgModule } from 'angular-ts-decorators'; +import { NgModule } from 'ng-metadata/core'; import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; import { NAME_PATTERNS } from "./constants/name-patterns.constant"; import * as Raven from "raven-js"; -import * as angular from 'angular'; -var quayDependencies: any[] = [ +var quayDependencies: string[] = [ 'chieffancypants.loadingBar', 'cfp.hotkeys', 'angular-tour', @@ -49,70 +48,80 @@ if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { @NgModule({ imports: quayDependencies, declarations: [], - providers: [] + providers: [ + provideConfig, + {provide: 'INJECTED_CONFIG', useValue: INJECTED_CONFIG}, + {provide: 'INJECTED_FEATURES', useValue: INJECTED_FEATURES}, + {provide: 'INJECTED_ENDPOINTS', useValue: INJECTED_ENDPOINTS}, + {provide: 'NAME_PATTERNS', useValue: NAME_PATTERNS}, + ] }) -export class QuayConfig { +export class QuayConfigModule { - public config($provide: ng.auto.IProvideService, - $injector: ng.auto.IInjectorService, - INJECTED_CONFIG: any, - cfpLoadingBarProvider: any, - $tooltipProvider: any, - $compileProvider: ng.ICompileProvider, - RestangularProvider: any): void { - cfpLoadingBarProvider.includeSpinner = false; - - // decorate the tooltip getter - var tooltipFactory: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; - $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { - if ('ontouchstart' in $window) { - var existing: any = tooltipFactory.apply(this, arguments); - return function(element) { - // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things - // (such as the datepicker), so we need to be specific when canceling it. - if (element !== undefined && element.attr('bs-tooltip') == null) { - return existing.apply(this, arguments); - } - }; - } - - return tooltipFactory.apply(this, arguments); - }; - - if (!INJECTED_CONFIG['DEBUG']) { - $compileProvider.debugInfoEnabled(false); - } - - // Configure compile provider to add additional URL prefixes to the sanitization list. We use - // these on the Contact page. - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); - - // Configure the API provider. - RestangularProvider.setBaseUrl('/api/v1/'); - - // Configure analytics. - if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { - let $analyticsProvider: any = $injector.get('$analyticsProvider'); - $analyticsProvider.virtualPageviews(true); - } - - // Configure sentry. - if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { - $provide.decorator("$exceptionHandler", function($delegate) { - return function(ex, cause) { - $delegate(ex, cause); - Raven.captureException(ex, {extra: {cause: cause}}); - }; - }); - } - } } -// TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 -angular - .module(QuayConfig.name) - .constant('NAME_PATTERNS', NAME_PATTERNS) - .constant('INJECTED_CONFIG', INJECTED_CONFIG) - .constant('INJECTED_FEATURES', INJECTED_FEATURES) - .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS); \ No newline at end of file +/** + * Provider function for the application configuration. + * See https://hotell.gitbooks.io/ng-metadata/content/docs/recipes/startup-logic.html + */ +provideConfig.$inject = [ + '$provide', + '$injector', + 'cfpLoadingBarProvider', + '$tooltipProvider', + '$compileProvider', + 'RestangularProvider', +]; +function provideConfig($provide: ng.auto.IProvideService, + $injector: ng.auto.IInjectorService, + cfpLoadingBarProvider: any, + $tooltipProvider: any, + $compileProvider: ng.ICompileProvider, + RestangularProvider: any): void { + cfpLoadingBarProvider.includeSpinner = false; + + // decorate the tooltip getter + var tooltipFactory: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; + $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { + if ('ontouchstart' in $window) { + var existing: any = tooltipFactory.apply(this, arguments); + return function(element) { + // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things + // (such as the datepicker), so we need to be specific when canceling it. + if (element !== undefined && element.attr('bs-tooltip') == null) { + return existing.apply(this, arguments); + } + }; + } + + return tooltipFactory.apply(this, arguments); + }; + + if (!INJECTED_CONFIG['DEBUG']) { + $compileProvider.debugInfoEnabled(false); + } + + // Configure compile provider to add additional URL prefixes to the sanitization list. We use + // these on the Contact page. + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); + + // Configure the API provider. + RestangularProvider.setBaseUrl('/api/v1/'); + + // Configure analytics. + if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { + let $analyticsProvider: any = $injector.get('$analyticsProvider'); + $analyticsProvider.virtualPageviews(true); + } + + // Configure sentry. + if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { + $provide.decorator("$exceptionHandler", function($delegate) { + return function(ex, cause) { + $delegate(ex, cause); + Raven.captureException(ex, {extra: {cause: cause}}); + }; + }); + } +} diff --git a/static/js/quay-pages.module.ts b/static/js/quay-pages.module.ts index 23ecc26a0..2bafbafb4 100644 --- a/static/js/quay-pages.module.ts +++ b/static/js/quay-pages.module.ts @@ -1,28 +1,13 @@ -import * as angular from 'angular'; -import { rpHeaderDirective, rpBodyDirective, rpSidebarDirective } from './directives/components/pages/repo-page/main'; import { PageServiceImpl } from './services/page/page.service.impl'; -import { NgModule } from 'angular-ts-decorators'; +import { rpHeaderDirective, rpBodyDirective, rpSidebarDirective } from './directives/components/pages/repo-page/main'; +import * as angular from 'angular'; /** - * Module containing registered application page/view components. + * TODO: Needed for non-TypeScript components/services to register themselves. Remove once they are migrated. */ -@NgModule({ - imports: [], - declarations: [], - providers: [ - PageServiceImpl, - ] -}) -export class quayPages { - -} - - -// TODO: Move component registration to @NgModule and remove this. -angular - .module(quayPages.name) +export const QuayPagesModule: ng.IModule = angular.module('quayPages', []) .constant('pages', new PageServiceImpl()) .directive('rpHeader', rpHeaderDirective) .directive('rpSidebar', rpSidebarDirective) - .directive('rpBody', rpBodyDirective); + .directive('rpBody', rpBodyDirective); \ No newline at end of file diff --git a/static/js/quay-routes.module.ts b/static/js/quay-routes.module.ts index ab3ce2bf4..d36b6b1bf 100644 --- a/static/js/quay-routes.module.ts +++ b/static/js/quay-routes.module.ts @@ -2,9 +2,9 @@ import { RouteBuilderImpl } from './services/route-builder/route-builder.service import { RouteBuilder } from './services/route-builder/route-builder.service'; import { PageService } from './services/page/page.service'; import * as ng from '@types/angular'; -import { NgModule } from 'angular-ts-decorators'; +import { NgModule } from 'ng-metadata/core'; import { INJECTED_FEATURES } from './constants/injected-values.constant'; -import { quayPages } from './quay-pages.module'; +import { QuayPagesModule } from './quay-pages.module'; /** @@ -12,138 +12,151 @@ import { quayPages } from './quay-pages.module'; */ @NgModule({ imports: [ - quayPages, + QuayPagesModule.name, 'ngRoute', ], declarations: [], - providers: [], + providers: [ + provideRoutes, + ], }) -export class QuayRoutes { +export class QuayRoutesModule { - public config($routeProvider: ng.route.IRouteProvider, - $locationProvider: ng.ILocationProvider, - pages: PageService): void { - $locationProvider.html5Mode(true); - - // WARNING WARNING WARNING - // If you add a route here, you must add a corresponding route in thr endpoints/web.py - // index rule to make sure that deep links directly deep into the app continue to work. - // WARNING WARNING WARNING - - var routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pages.$get()); - - if (INJECTED_FEATURES.SUPER_USERS) { - // QE Management - routeBuilder.route('/superuser/', 'superuser') - // QE Setup - .route('/setup/', 'setup'); - } - - routeBuilder - // Application View - .route('/application/:namespace/:name', 'app-view') - - // Repo List - .route('/application/', 'app-list') - - // Repository View - .route('/repository/:namespace/:name', 'repo-view') - .route('/repository/:namespace/:name/tag/:tag', 'repo-view') - - // Image View - .route('/repository/:namespace/:name/image/:image', 'image-view') - - // Repo Build View - .route('/repository/:namespace/:name/build/:buildid', 'build-view') - - // Repo Trigger View - .route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup') - - // Create repository notification - .route('/repository/:namespace/:name/create-notification', 'create-repository-notification') - - // Repo List - .route('/repository/', 'repo-list') - - // Organizations - .route('/organizations/', 'organizations') - - // New Organization - .route('/organizations/new/', 'new-organization') - - // View Organization - .route('/organization/:orgname', 'org-view') - - // View Organization Team - .route('/organization/:orgname/teams/:teamname', 'team-view') - - // Organization View Application - .route('/organization/:orgname/application/:clientid', 'manage-application') - - // View Organization Billing - .route('/organization/:orgname/billing', 'billing') - - // View Organization Billing Invoices - .route('/organization/:orgname/billing/invoices', 'invoices') - - // View User - .route('/user/:username', 'user-view') - - // View User Billing - .route('/user/:username/billing', 'billing') - - // View User Billing Invoices - .route('/user/:username/billing/invoices', 'invoices') - - // Sign In - .route('/signin/', 'signin') - - // New Repository - .route('/new/', 'new-repo') - - // Plans - .route('/plans/', 'plans') - - // Tutorial - .route('/tutorial/', 'tutorial') - - // Contact - .route('/contact/', 'contact') - - // About - .route('/about/', 'about') - - // Security - .route('/security/', 'security') - - // TOS - .route('/tos', 'tos') - - // Privacy - .route('/privacy', 'privacy') - - // Change username - .route('/updateuser', 'update-user') - - // Landing Page - .route('/', 'landing') - - // Tour - .route('/tour/', 'tour') - .route('/tour/features', 'tour') - .route('/tour/organizations', 'tour') - .route('/tour/enterprise', 'tour') - - // Confirm Invite - .route('/confirminvite', 'confirm-invite') - - // Public Repo Experiments - .route('/__exp/publicRepo', 'public-repo-exp') - - // 404/403 - .route('/:catchall', 'error-view') - .route('/:catch/:all', 'error-view') - .route('/:catch/:all/:things', 'error-view') - .route('/:catch/:all/:things/:here', 'error-view'); - } +} + + +/** + * Provider function for setting up client-side routing. + * See https://hotell.gitbooks.io/ng-metadata/content/docs/recipes/startup-logic.html + */ +provideRoutes.$inject = [ + '$routeProvider', + '$locationProvider', + 'pages', +]; +function provideRoutes($routeProvider: ng.route.IRouteProvider, + $locationProvider: ng.ILocationProvider, + pageServiceProvider: PageService): void { + $locationProvider.html5Mode(true); + + // WARNING WARNING WARNING + // If you add a route here, you must add a corresponding route in thr endpoints/web.py + // index rule to make sure that deep links directly deep into the app continue to work. + // WARNING WARNING WARNING + + const routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pageServiceProvider.$get()); + + if (INJECTED_FEATURES.SUPER_USERS) { + // QE Management + routeBuilder.route('/superuser/', 'superuser') + // QE Setup + .route('/setup/', 'setup'); + } + + routeBuilder + // Application View + .route('/application/:namespace/:name', 'app-view') + + // Repo List + .route('/application/', 'app-list') + + // Repository View + .route('/repository/:namespace/:name', 'repo-view') + .route('/repository/:namespace/:name/tag/:tag', 'repo-view') + + // Image View + .route('/repository/:namespace/:name/image/:image', 'image-view') + + // Repo Build View + .route('/repository/:namespace/:name/build/:buildid', 'build-view') + + // Repo Trigger View + .route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup') + + // Create repository notification + .route('/repository/:namespace/:name/create-notification', 'create-repository-notification') + + // Repo List + .route('/repository/', 'repo-list') + + // Organizations + .route('/organizations/', 'organizations') + + // New Organization + .route('/organizations/new/', 'new-organization') + + // View Organization + .route('/organization/:orgname', 'org-view') + + // View Organization Team + .route('/organization/:orgname/teams/:teamname', 'team-view') + + // Organization View Application + .route('/organization/:orgname/application/:clientid', 'manage-application') + + // View Organization Billing + .route('/organization/:orgname/billing', 'billing') + + // View Organization Billing Invoices + .route('/organization/:orgname/billing/invoices', 'invoices') + + // View User + .route('/user/:username', 'user-view') + + // View User Billing + .route('/user/:username/billing', 'billing') + + // View User Billing Invoices + .route('/user/:username/billing/invoices', 'invoices') + + // Sign In + .route('/signin/', 'signin') + + // New Repository + .route('/new/', 'new-repo') + + // Plans + .route('/plans/', 'plans') + + // Tutorial + .route('/tutorial/', 'tutorial') + + // Contact + .route('/contact/', 'contact') + + // About + .route('/about/', 'about') + + // Security + .route('/security/', 'security') + + // TOS + .route('/tos', 'tos') + + // Privacy + .route('/privacy', 'privacy') + + // Change username + .route('/updateuser', 'update-user') + + // Landing Page + .route('/', 'landing') + + // Tour + .route('/tour/', 'tour') + .route('/tour/features', 'tour') + .route('/tour/organizations', 'tour') + .route('/tour/enterprise', 'tour') + + // Confirm Invite + .route('/confirminvite', 'confirm-invite') + + // Public Repo Experiments + .route('/__exp/publicRepo', 'public-repo-exp') + + // 404/403 + .route('/:catchall', 'error-view') + .route('/:catch/:all', 'error-view') + .route('/:catch/:all/:things', 'error-view') + .route('/:catch/:all/:things/:here', 'error-view'); } diff --git a/static/js/quay-run.module.ts b/static/js/quay-run.module.ts deleted file mode 100644 index 710235c7b..000000000 --- a/static/js/quay-run.module.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { NgModule } from 'angular-ts-decorators'; -import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; -import { NAME_PATTERNS } from "./constants/name-patterns.constant"; -import * as angular from 'angular'; - - -var quayDependencies: any[] = [ - 'chieffancypants.loadingBar', - 'cfp.hotkeys', - 'angular-tour', - 'restangular', - 'angularMoment', - 'mgcrea.ngStrap', - 'ngCookies', - 'ngSanitize', - 'angular-md5', - 'pasvaz.bindonce', - 'ansiToHtml', - 'core-ui', - 'core-config-setup', - 'infinite-scroll', - 'react' -]; - -if (INJECTED_CONFIG && (INJECTED_CONFIG.MIXPANEL_KEY || - INJECTED_CONFIG.MUNCHKIN_KEY || - INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY)) { - quayDependencies.push('angulartics'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { - quayDependencies.push('angulartics.mixpanel'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.MUNCHKIN_KEY) { - quayDependencies.push('angulartics.marketo'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY) { - quayDependencies.push('angulartics.google.analytics'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { - quayDependencies.push('vcRecaptcha'); -} - - -/** - * Module for application-wide configuration. - */ -@NgModule({ - imports: quayDependencies, - declarations: [], - providers: [] -}) -export class QuayRun { - - public run($rootScope: QuayRunScope, - Restangular: any, - PlanService: any, - $http: ng.IHttpService, - CookieService: any, - Features: any, - $anchorScroll: ng.IAnchorScrollService, - MetaService: any, - INJECTED_CONFIG: any): void { - var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; - - // Handle session security. - Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], - {'_csrf_token': (window).__token || ''}); - - // Handle session expiration. - Restangular.setErrorInterceptor(function(response) { - if (response !== undefined && response.status == 503) { - ($('#cannotContactService')).modal({}); - return false; - } - - if (response !== undefined && response.status == 500) { - window.location.href = '/500'; - return false; - } - - if (response !== undefined && !response.data) { - return true; - } - - var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; - if (response !== undefined && response.status == 401 && - invalid_token && - response.data['session_required'] !== false) { - ($('#sessionexpiredModal')).modal({}); - return false; - } - - return true; - }); - - // Check if we need to redirect based on a previously chosen plan. - var result = PlanService.handleNotedPlan(); - - // Check to see if we need to show a redirection page. - var redirectUrl = CookieService.get('quay.redirectAfterLoad'); - CookieService.clear('quay.redirectAfterLoad'); - - if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { - (window).location = redirectUrl; - return; - } - - $rootScope.$watch('description', function(description: string) { - if (!description) { - description = `Hosted private docker repositories. Includes full user management and history. - Free for public repositories.`; - } - - // Note: We set the content of the description tag manually here rather than using Angular binding - // because we need the tag to have a default description that is not of the form "{{ description }}", - // we read by tools that do not properly invoke the Angular code. - $('#descriptionTag').attr('content', description); - }); - - // Listen for scope changes and update the title and description accordingly. - $rootScope.$watch(function() { - var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; - $rootScope.title = title; - - var description = MetaService.getDescription($rootScope.currentPage) || ''; - if ($rootScope.description != description) { - $rootScope.description = description; - } - }); - - $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { - $rootScope.current = current.$$route; - $rootScope.currentPage = current; - $rootScope.pageClass = ''; - - if (!current.$$route) { return; } - - var pageClass = current.$$route.pageClass || ''; - if (typeof pageClass != 'string') { - pageClass = pageClass(Features); - } - - $rootScope.pageClass = pageClass; - $rootScope.newLayout = !!current.$$route.newLayout; - $rootScope.fixFooter = !!current.$$route.fixFooter; - - $anchorScroll(); - }); - - var initallyChecked: boolean = false; - (window).__isLoading = function() { - if (!initallyChecked) { - initallyChecked = true; - return true; - } - return $http.pendingRequests.length > 0; - }; - } -} - - -interface QuayRunScope extends ng.IRootScopeService { - currentPage: any; - current: any; - title: any; - description: string, - pageClass: any; - newLayout: any; - fixFooter: any; -} - - -// TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 -angular - .module(QuayRun.name) - .constant('NAME_PATTERNS', NAME_PATTERNS) - .constant('INJECTED_CONFIG', INJECTED_CONFIG) - .constant('INJECTED_FEATURES', INJECTED_FEATURES) - .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS); \ No newline at end of file diff --git a/static/js/quay-run.ts b/static/js/quay-run.ts new file mode 100644 index 000000000..3001f97c1 --- /dev/null +++ b/static/js/quay-run.ts @@ -0,0 +1,133 @@ +import { INJECTED_CONFIG } from "./constants/injected-values.constant"; + + +/** + * Provider function for the application runtime configuration. + * See https://hotell.gitbooks.io/ng-metadata/content/docs/recipes/startup-logic.html + */ +provideRun.$inject = [ + '$rootScope', + 'Restangular', + 'PlanService', + '$http', + 'CookieService', + 'Features', + '$anchorScroll', + 'MetaService', +]; +export function provideRun($rootScope: QuayRunScope, + Restangular: any, + PlanService: any, + $http: ng.IHttpService, + CookieService: any, + Features: any, + $anchorScroll: ng.IAnchorScrollService, + MetaService: any): void { + const defaultTitle: string = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; + + // Handle session security. + Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], + {'_csrf_token': (window).__token || ''}); + + // Handle session expiration. + Restangular.setErrorInterceptor(function(response) { + if (response !== undefined && response.status == 503) { + ($('#cannotContactService')).modal({}); + return false; + } + + if (response !== undefined && response.status == 500) { + window.location.href = '/500'; + return false; + } + + if (response !== undefined && !response.data) { + return true; + } + + const invalid_token: boolean = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; + if (response !== undefined && + response.status == 401 && + invalid_token && + response.data['session_required'] !== false) { + ($('#sessionexpiredModal')).modal({}); + return false; + } + + return true; + }); + + // Check if we need to redirect based on a previously chosen plan. + const result: boolean = PlanService.handleNotedPlan(); + + // Check to see if we need to show a redirection page. + const redirectUrl: string = CookieService.get('quay.redirectAfterLoad'); + CookieService.clear('quay.redirectAfterLoad'); + + if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { + (window).location = redirectUrl; + return; + } + + $rootScope.$watch('description', function(description: string) { + if (!description) { + description = `Hosted private docker repositories. Includes full user management and history. + Free for public repositories.`; + } + + // Note: We set the content of the description tag manually here rather than using Angular binding + // because we need the tag to have a default description that is not of the form "{{ description }}", + // we read by tools that do not properly invoke the Angular code. + $('#descriptionTag').attr('content', description); + }); + + // Listen for scope changes and update the title and description accordingly. + $rootScope.$watch(function() { + const title: string = MetaService.getTitle($rootScope.currentPage) || defaultTitle; + $rootScope.title = title; + + const description: string = MetaService.getDescription($rootScope.currentPage) || ''; + if ($rootScope.description != description) { + $rootScope.description = description; + } + }); + + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + $rootScope.current = current.$$route; + $rootScope.currentPage = current; + $rootScope.pageClass = ''; + + if (!current.$$route) { return; } + + var pageClass: string | Function = current.$$route.pageClass || ''; + if (typeof pageClass != 'string') { + pageClass = pageClass(Features); + } + + $rootScope.pageClass = pageClass; + $rootScope.newLayout = !!current.$$route.newLayout; + $rootScope.fixFooter = !!current.$$route.fixFooter; + + $anchorScroll(); + }); + + var initallyChecked: boolean = false; + (window).__isLoading = function() { + if (!initallyChecked) { + initallyChecked = true; + return true; + } + return $http.pendingRequests.length > 0; + }; +} + + +interface QuayRunScope extends ng.IRootScopeService { + currentPage: any; + current: any; + title: any; + description: string, + pageClass: any; + newLayout: any; + fixFooter: any; +} diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 01838ab82..36006547e 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -1,28 +1,24 @@ -import * as angular from "angular"; -import 'core-js'; import { ViewArrayImpl } from "./services/view-array/view-array.impl"; -import { NAME_PATTERNS } from "./constants/name-patterns.constant"; -import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; import { RegexMatchViewComponent } from "./directives/ui/regex-match-view/regex-match-view.component"; -import { NgModule } from "angular-ts-decorators"; -import { QuayRoutes } from "./quay-routes.module"; +import { NgModule } from 'ng-metadata/core'; +import { QuayRoutesModule } from "./quay-routes.module"; import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-select/dockerfile-path-select.component'; import { ContextPathSelectComponent } from './directives/ui/context-path-select/context-path-select.component'; import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component'; import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component'; import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-workflow.component'; import { LinearWorkflowSectionComponent } from './directives/ui/linear-workflow/linear-workflow-section.component'; +import { QuayConfigModule } from './quay-config.module'; import { AppPublicViewComponent } from './directives/ui/app-public-view/app-public-view.component'; import { VisibilityIndicatorComponent } from './directives/ui/visibility-indicator/visibility-indicator.component'; import { CorTableComponent } from './directives/ui/cor-table/cor-table.component'; import { CorTableColumn } from './directives/ui/cor-table/cor-table-col.component'; import { ChannelIconComponent } from './directives/ui/channel-icon/channel-icon.component'; -import { QuayConfig } from './quay-config.module'; -import { QuayRun } from './quay-run.module'; import { BuildServiceImpl } from './services/build/build.service.impl'; import { AvatarServiceImpl } from './services/avatar/avatar.service.impl'; import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl'; import { DataFileServiceImpl } from './services/datafile/datafile.service.impl'; +import { UtilServiceImpl } from './services/util/util.service.impl'; /** @@ -30,9 +26,8 @@ import { DataFileServiceImpl } from './services/datafile/datafile.service.impl'; */ @NgModule({ imports: [ - QuayRoutes, - QuayConfig, - QuayRun, + QuayRoutesModule, + QuayConfigModule, ], declarations: [ RegexMatchViewComponent, @@ -54,17 +49,10 @@ import { DataFileServiceImpl } from './services/datafile/datafile.service.impl'; AvatarServiceImpl, DockerfileServiceImpl, DataFileServiceImpl, + UtilServiceImpl, + {provide: 'fileReaderFactory', useValue: () => () => new FileReader()}, ], }) -export class quay { +export class QuayModule { } - -// TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 -angular - .module(quay.name) - .factory("fileReaderFactory", () => () => new FileReader()) - .constant('NAME_PATTERNS', NAME_PATTERNS) - .constant('INJECTED_CONFIG', INJECTED_CONFIG) - .constant('INJECTED_FEATURES', INJECTED_FEATURES) - .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS); \ No newline at end of file diff --git a/static/js/services/avatar/avatar.service.impl.ts b/static/js/services/avatar/avatar.service.impl.ts index 673cec473..a9eb54d36 100644 --- a/static/js/services/avatar/avatar.service.impl.ts +++ b/static/js/services/avatar/avatar.service.impl.ts @@ -1,5 +1,5 @@ import { AvatarService } from './avatar.service'; -import { Injectable } from 'angular-ts-decorators'; +import { Injectable, Inject } from 'ng-metadata/core'; @Injectable(AvatarService.name) @@ -7,7 +7,8 @@ export class AvatarServiceImpl implements AvatarService { private cache: {[cacheKey: string]: string} = {}; - constructor(private Config: any, private md5: any) { + constructor(@Inject('Config') private Config: any, + @Inject('md5') private md5: any) { } diff --git a/static/js/services/build/build.service.impl.ts b/static/js/services/build/build.service.impl.ts index 53cca4901..6183ea1f7 100644 --- a/static/js/services/build/build.service.impl.ts +++ b/static/js/services/build/build.service.impl.ts @@ -1,5 +1,5 @@ import { BuildService } from './build.service'; -import { Injectable } from 'angular-ts-decorators'; +import { Injectable } from 'ng-metadata/core'; @Injectable(BuildService.name) diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts index be5ce180f..f9bc83ed8 100644 --- a/static/js/services/datafile/datafile.service.impl.ts +++ b/static/js/services/datafile/datafile.service.impl.ts @@ -1,5 +1,5 @@ import { DataFileService } from './datafile.service'; -import { Injectable } from 'angular-ts-decorators'; +import { Injectable, Inject } from 'ng-metadata/core'; declare const JSZip: (buf: any) => void; declare const Zlib: any; declare const Untar: (uint8Array: Uint8Array) => void; @@ -8,7 +8,7 @@ declare const Untar: (uint8Array: Uint8Array) => void; @Injectable(DataFileService.name) export class DataFileServiceImpl implements DataFileService { - constructor(private fileReaderFactory: () => FileReader) { + constructor(@Inject('fileReaderFactory') private fileReaderFactory: () => FileReader) { } diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index 54adc51c8..37db16b52 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -1,14 +1,14 @@ import { DockerfileService, DockerfileInfo } from './dockerfile.service'; -import { Injectable } from 'angular-ts-decorators'; +import { Injectable, Inject } from 'ng-metadata/core'; import { DataFileService } from '../datafile/datafile.service'; @Injectable(DockerfileService.name) export class DockerfileServiceImpl implements DockerfileService { - constructor(private DataFileService: DataFileService, - private Config: any, - private fileReaderFactory: () => FileReader) { + constructor(@Inject(DataFileService.name) private DataFileService: DataFileService, + @Inject('Config') private Config: any, + @Inject('fileReaderFactory') private fileReaderFactory: () => FileReader) { } diff --git a/static/js/services/page/page.service.impl.ts b/static/js/services/page/page.service.impl.ts index d52818899..a018bf6ed 100644 --- a/static/js/services/page/page.service.impl.ts +++ b/static/js/services/page/page.service.impl.ts @@ -1,4 +1,4 @@ -import { Injectable } from 'angular-ts-decorators'; +import { Injectable } from 'ng-metadata/core'; import { PageService } from './page.service'; diff --git a/static/js/services/route-builder/route-builder.service.impl.ts b/static/js/services/route-builder/route-builder.service.impl.ts index 12780952a..901dd7c15 100644 --- a/static/js/services/route-builder/route-builder.service.impl.ts +++ b/static/js/services/route-builder/route-builder.service.impl.ts @@ -1,6 +1,9 @@ import { RouteBuilder } from './route-builder.service'; +import { Injectable, Inject } from 'ng-metadata/core'; +import { PageService } from '../page/page.service'; +@Injectable(RouteBuilder.name) export class RouteBuilderImpl implements RouteBuilder { public currentProfile: string = 'layout'; @@ -12,7 +15,8 @@ export class RouteBuilderImpl implements RouteBuilder { ]; - constructor(private routeProvider: ng.route.IRouteProvider, private pages: any) { + constructor(@Inject('routeProvider') private routeProvider: ng.route.IRouteProvider, + @Inject('pages') private pages: PageService) { for (let i = 0; i < this.profiles.length; ++i) { if (this.profiles[i].id == this.currentProfile) { this.profiles = this.profiles.slice(i); diff --git a/static/js/services/route-builder/route-builder.service.spec.ts b/static/js/services/route-builder/route-builder.service.spec.ts index fd09a277a..cb0facf5a 100644 --- a/static/js/services/route-builder/route-builder.service.spec.ts +++ b/static/js/services/route-builder/route-builder.service.spec.ts @@ -1,4 +1,5 @@ import { RouteBuilderImpl } from './route-builder.service.impl'; +import { PageService } from '../page/page.service'; describe("Service: RouteBuilderImpl", () => { diff --git a/static/js/services/util/util.service.impl.spec.ts b/static/js/services/util/util.service.impl.spec.ts new file mode 100644 index 000000000..92748be13 --- /dev/null +++ b/static/js/services/util/util.service.impl.spec.ts @@ -0,0 +1,40 @@ +import { UtilServiceImpl } from './util.service.impl'; + + +describe("UtilServiceImpl", () => { + var utilServiceImpl: UtilServiceImpl; + var $sanitizeMock: ng.sanitize.ISanitizeService; + + beforeEach(() => { + $sanitizeMock = jasmine.createSpy('$sanitizeSpy').and.returnValue(""); + utilServiceImpl = new UtilServiceImpl($sanitizeMock); + }); + + describe("isAdBlockEnabled", () => { + // TODO + }); + + describe("isEmailAddress", () => { + // TODO + }); + + describe("getMarkedDown", () => { + // TODO + }); + + describe("getFirstMarkdownLineAsText", () => { + // TODO + }); + + describe("escapeHtmlString", () => { + // TODO + }); + + describe("getRestUrl", () => { + // TODO + }); + + describe("textToSafeHtml", () => { + // TODO + }); +}); diff --git a/static/js/services/util/util.service.impl.ts b/static/js/services/util/util.service.impl.ts new file mode 100644 index 000000000..390b5913c --- /dev/null +++ b/static/js/services/util/util.service.impl.ts @@ -0,0 +1,39 @@ +import { Injectable, Inject } from 'ng-metadata/core'; +import { UtilService } from './util.service'; + + +@Injectable(UtilService.name) +export class UtilServiceImpl implements UtilService { + + constructor(@Inject('$sanitize') private $sanitize: ng.sanitize.ISanitizeService) { + + } + + public isAdBlockEnabled(callback: (isEnabled: boolean) => void): void { + + } + + public isEmailAddress(str: string): boolean { + return null; + } + + public getMarkedDown(str: string): string { + return null; + } + + public getFirstMarkdownLineAsText(commentString: string, placeholderNeeded: boolean): string { + return null; + } + + public escapeHtmlString(text: string): string { + return null; + } + + public getRestUrl(args: any[]): string { + return null; + } + + public textToSafeHtml(text: string): string { + return null; + } +} \ No newline at end of file diff --git a/static/js/services/util/util.service.ts b/static/js/services/util/util.service.ts new file mode 100644 index 000000000..688421a40 --- /dev/null +++ b/static/js/services/util/util.service.ts @@ -0,0 +1,19 @@ +/** + * Service which exposes various utility methods. + */ +export abstract class UtilService { + + public abstract isAdBlockEnabled(callback: (isEnabled: boolean) => void): void; + + public abstract isEmailAddress(str: string): boolean; + + public abstract getMarkedDown(str: string): string; + + public abstract getFirstMarkdownLineAsText(commentString: string, placeholderNeeded: boolean): string; + + public abstract escapeHtmlString(text: string): string; + + public abstract getRestUrl(args: any[]): string; + + public abstract textToSafeHtml(text: string): string; +} diff --git a/static/js/services/view-array/view-array.impl.ts b/static/js/services/view-array/view-array.impl.ts index 939d50e5c..60032dab4 100644 --- a/static/js/services/view-array/view-array.impl.ts +++ b/static/js/services/view-array/view-array.impl.ts @@ -1,6 +1,5 @@ import { ViewArray } from './view-array'; -import { Inject } from '../../decorators/inject/inject.decorator'; -import { Injectable } from 'angular-ts-decorators'; +import { Injectable, Inject } from 'ng-metadata/core'; @Injectable(ViewArray.name) @@ -15,7 +14,7 @@ export class ViewArrayImpl implements ViewArray { private currentIndex: number; private additionalCount: number = 20; - constructor(@Inject('$interval') private interval: any) { + constructor(@Inject('$interval') private $interval: ng.IIntervalService) { this.isVisible = false; this.visibleEntries = null; this.hasEntries = false; @@ -62,7 +61,7 @@ export class ViewArrayImpl implements ViewArray { } public create(): ViewArrayImpl { - return new ViewArrayImpl(this.interval); + return new ViewArrayImpl(this.$interval); } private showAdditionalEntries(): void { @@ -83,14 +82,14 @@ export class ViewArrayImpl implements ViewArray { return; } - this.timerRef = this.interval(() => { + this.timerRef = this.$interval(() => { this.showAdditionalEntries(); }, 10); } private stopTimer(): void { if (this.timerRef) { - this.interval.cancel(this.timerRef); + this.$interval.cancel(this.timerRef); this.timerRef = null; } } diff --git a/static/partials/trigger-setup.html b/static/partials/trigger-setup.html index 7f1fad28d..6f5e36e55 100644 --- a/static/partials/trigger-setup.html +++ b/static/partials/trigger-setup.html @@ -46,7 +46,7 @@
+ (activate-trigger)="activateTrigger($event)">
@@ -54,7 +54,7 @@ + (activate-trigger)="activateTrigger($event)">
diff --git a/tsconfig.json b/tsconfig.json index 9ec73f044..58312dc96 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "module": "commonjs", "outDir": "./build/", "target": "es5", + "lib": ["es2017", "dom"], "experimentalDecorators": true, "sourceMap": true, "paths": { diff --git a/webpack.config.js b/webpack.config.js index 68f51889c..189aa3c2e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,7 +2,7 @@ var webpack = require('webpack'); var path = require("path"); var config = { - entry: "./static/js/quay.module.ts", + entry: "./static/js/main.ts", output: { path: path.resolve(__dirname, "static/build"), filename: "bundle.js" diff --git a/yarn.lock b/yarn.lock index bfeb362ba..3486c702d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,6 +26,10 @@ dependencies: "@types/jquery" "*" +"@types/core-js@^0.9.39": + version "0.9.39" + resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-0.9.39.tgz#9b3d9869c2b7de02d372d32d6c6d9f5db3178c65" + "@types/es6-shim@^0.31.32": version "0.31.32" resolved "https://registry.yarnpkg.com/@types/es6-shim/-/es6-shim-0.31.32.tgz#8196c09e1e40ac977c713bf258090989f501d8ff" @@ -2473,6 +2477,10 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +ng-metadata@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ng-metadata/-/ng-metadata-4.0.1.tgz#c367018ac9e5c214c57b987e82940a30fd4b9c67" + node-fetch@^1.0.1: version "1.6.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" @@ -3488,6 +3496,12 @@ ripemd160@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" +rxjs@^5.0.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.2.0.tgz#db537de8767c05fa73721587a29e0085307d318b" + dependencies: + symbol-observable "^1.0.1" + sass-graph@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.1.2.tgz#965104be23e8103cb7e5f710df65935b317da57b" @@ -3780,6 +3794,10 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" +symbol-observable@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + tapable@^0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"