From 4f96ab53533d48ce4bd8e0a626aa19bc01649956 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 18 Jan 2017 15:46:37 -0800 Subject: [PATCH 01/19] refactoring to TypeScript and ES6 modules --- karma.conf.js | 11 +- package.json | 12 +- .../angular-view-array/angular-view-array.js | 104 ----------------- ...ray.spec.js => angular-view-array.spec.ts} | 81 ++++++------- .../angular-view-array/angular-view-array.ts | 108 ++++++++++++++++++ .../angular-view-array/view-array.impl.ts | 90 +++++++++++++++ .../services/angular-view-array/view-array.ts | 12 ++ webpack.config.js | 11 +- 8 files changed, 274 insertions(+), 155 deletions(-) delete mode 100644 static/js/services/angular-view-array/angular-view-array.js rename static/js/services/angular-view-array/{angular-view-array.spec.js => angular-view-array.spec.ts} (55%) create mode 100644 static/js/services/angular-view-array/angular-view-array.ts create mode 100644 static/js/services/angular-view-array/view-array.impl.ts create mode 100644 static/js/services/angular-view-array/view-array.ts diff --git a/karma.conf.js b/karma.conf.js index 3a917a709..73149f50f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,4 +1,7 @@ -module.exports = function (config) { +var webpackConfig = require('./webpack.config'); + + +module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine'], @@ -26,6 +29,7 @@ module.exports = function (config) { // Application resources 'static/js/**/*.js', + 'static/js/**/*.ts', // Tests 'static/test/**/*.js', @@ -36,8 +40,11 @@ module.exports = function (config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], + 'static/js/**/*.ts': ['webpack'], + }, + webpack: { + module: webpackConfig.module, }, - webpack: {}, webpackMiddleware: { stats: 'errors-only' }, diff --git a/package.json b/package.json index 36de3d949..4d87b1c97 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,6 @@ }, "homepage": "https://github.com/coreos-inc/quay#readme", "dependencies": { - "@types/angular": "1.5.16", - "@types/react": "0.14.39", - "@types/react-dom": "0.14.17", "angular": "1.5.3", "angular-animate": "^1.5.3", "angular-cookies": "^1.5.3", @@ -36,7 +33,13 @@ "underscore": "^1.5.2" }, "devDependencies": { + "@types/angular": "1.5.16", + "@types/angular-mocks": "^1.5.8", + "@types/jasmine": "^2.5.41", + "@types/react": "0.14.39", + "@types/react-dom": "0.14.17", "angular-mocks": "^1.5.3", + "css-loader": "0.25.0", "jasmine-core": "^2.5.2", "karma": "^0.13.22", "karma-chrome-launcher": "^2.0.0", @@ -45,12 +48,11 @@ "karma-jasmine": "^0.3.8", "karma-phantomjs-launcher": "^1.0.0", "karma-webpack": "^1.8.1", - "css-loader": "0.25.0", "node-sass": "3.10.1", + "phantomjs-prebuilt": "^2.1.7", "sass-loader": "4.0.2", "source-map-loader": "0.1.5", "style-loader": "0.13.1", - "phantomjs-prebuilt": "^2.1.7", "ts-loader": "0.9.5", "typescript": "2.0.3", "typings": "1.4.0", diff --git a/static/js/services/angular-view-array/angular-view-array.js b/static/js/services/angular-view-array/angular-view-array.js deleted file mode 100644 index d23ae3949..000000000 --- a/static/js/services/angular-view-array/angular-view-array.js +++ /dev/null @@ -1,104 +0,0 @@ -(function() { - 'use strict'; - - /** - * Specialized wrapper around array which provides a toggle() method for viewing the contents of the - * array in a manner that is asynchronously filled in over a short time period. This prevents long - * pauses in the UI for ngRepeat's when the array is significant in size. - */ - angular - .module('quay') - .factory('AngularViewArray', factory); - - factory.$inject = [ - '$interval' - ]; - - function factory($interval) { - var ADDTIONAL_COUNT = 20; - - function _ViewArray() { - this.isVisible = false; - this.visibleEntries = null; - this.hasEntries = false; - this.entries = []; - this.hasHiddenEntries = false; - - this.timerRef_ = null; - this.currentIndex_ = 0; - } - - _ViewArray.prototype.length = function() { - return this.entries.length; - }; - - _ViewArray.prototype.get = function(index) { - return this.entries[index]; - }; - - _ViewArray.prototype.push = function(elem) { - this.entries.push(elem); - this.hasEntries = true; - - if (this.isVisible) { - this.startTimer_(); - } - }; - - _ViewArray.prototype.toggle = function() { - this.setVisible(!this.isVisible); - }; - - _ViewArray.prototype.setVisible = function(newState) { - this.isVisible = newState; - - this.visibleEntries = []; - this.currentIndex_ = 0; - - if (newState) { - this.showAdditionalEntries_(); - this.startTimer_(); - } else { - this.stopTimer_(); - } - }; - - _ViewArray.prototype.showAdditionalEntries_ = function() { - var i = 0; - for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) { - this.visibleEntries.push(this.entries[i]); - } - - this.currentIndex_ = i; - this.hasHiddenEntries = this.currentIndex_ < this.entries.length; - if (this.currentIndex_ >= this.entries.length) { - this.stopTimer_(); - } - }; - - _ViewArray.prototype.startTimer_ = function() { - if (this.timerRef_) { return; } - - var that = this; - this.timerRef_ = $interval(function() { - that.showAdditionalEntries_(); - }, 10); - }; - - _ViewArray.prototype.stopTimer_ = function() { - if (this.timerRef_) { - $interval.cancel(this.timerRef_); - this.timerRef_ = null; - } - }; - - var service = { - 'create': function() { - return new _ViewArray(); - } - }; - - return service; - } - -})(); \ No newline at end of file diff --git a/static/js/services/angular-view-array/angular-view-array.spec.js b/static/js/services/angular-view-array/angular-view-array.spec.ts similarity index 55% rename from static/js/services/angular-view-array/angular-view-array.spec.js rename to static/js/services/angular-view-array/angular-view-array.spec.ts index 314a3b5a1..ce30f9268 100644 --- a/static/js/services/angular-view-array/angular-view-array.spec.js +++ b/static/js/services/angular-view-array/angular-view-array.spec.ts @@ -1,44 +1,47 @@ -describe("Service: AngularViewArray", function() { - var angularViewArray; +import IInjectorService = angular.auto.IInjectorService; - beforeEach(module('quay')); - beforeEach(inject(function($injector) { +describe("Service: AngularViewArray", () => { + var angularViewArray: any; + + beforeEach(angular.mock.module('quay')); + + beforeEach(inject(($injector: IInjectorService) => { angularViewArray = $injector.get('AngularViewArray'); })); - describe("create", function() { + describe("create", () => { - it("returns a ViewArray object", function() { - var viewArray = angularViewArray.create(); + it("returns a ViewArray object", () => { + var viewArray: any = angularViewArray.create(); expect(viewArray).toBeDefined(); }); - describe("returned ViewArray object", function() { - var viewArray; + describe("returned ViewArray object", () => { + var viewArray: any; - beforeEach(function() { + beforeEach(() => { viewArray = angularViewArray.create(); }); - describe("constructor", function() { + describe("constructor", () => { // TODO }); - describe("length", function() { + describe("length", () => { - it("returns the number of entries", function() { + it("returns the number of entries", () => { viewArray.entries = [{}, {}, {}]; expect(viewArray.length()).toEqual(viewArray.entries.length); }); }); - describe("get", function() { + describe("get", () => { - it("returns the entry at a given index", function() { - var index = 8; + it("returns the entry at a given index", () => { + var index: number = 8; viewArray.entries = new Array(10); viewArray.entries[index] = 3; @@ -46,33 +49,33 @@ describe("Service: AngularViewArray", function() { }); }); - describe("push", function() { + describe("push", () => { - it("adds given element to the end of entries", function() { - var element = 3; - var originalLength = viewArray.length(); + it("adds given element to the end of entries", () => { + var element: number = 3; + var originalLength: number = viewArray.length(); viewArray.push(element); expect(viewArray.entries.length).toEqual(originalLength + 1); expect(viewArray.get(originalLength)).toEqual(element); }); - it("sets 'hasEntries' to true", function() { + it("sets 'hasEntries' to true", () => { viewArray.push(2); expect(viewArray.hasEntries).toBe(true); }); - it("starts timer if 'isVisible' is true", function() { - spyOn(viewArray, "startTimer_").and.returnValue(); + it("starts timer if 'isVisible' is true", () => { + spyOn(viewArray, "startTimer_").and.returnValue(null); viewArray.isVisible = true; viewArray.push(2); expect(viewArray.startTimer_).toHaveBeenCalled(); }); - it("does not start timer if 'isVisible' is false", function() { - spyOn(viewArray, "startTimer_").and.returnValue(); + it("does not start timer if 'isVisible' is false", () => { + spyOn(viewArray, "startTimer_").and.returnValue(null); viewArray.isVisible = false; viewArray.push(2); @@ -80,16 +83,16 @@ describe("Service: AngularViewArray", function() { }); }); - describe("toggle", function() { + describe("toggle", () => { - it("sets 'isVisible' to false if currently true", function() { + it("sets 'isVisible' to false if currently true", () => { viewArray.isVisible = true; viewArray.toggle(); expect(viewArray.isVisible).toBe(false); }); - it("sets 'isVisible' to true if currently false", function() { + it("sets 'isVisible' to true if currently false", () => { viewArray.isVisible = false; viewArray.toggle(); @@ -97,43 +100,43 @@ describe("Service: AngularViewArray", function() { }); }); - describe("setVisible", function() { + describe("setVisible", () => { - it("sets 'isVisible' to false if given false", function() { + it("sets 'isVisible' to false if given false", () => { viewArray.setVisible(false); expect(viewArray.isVisible).toBe(false); }); - it("sets 'visibleEntries' to empty array if given false", function() { + it("sets 'visibleEntries' to empty array if given false", () => { viewArray.setVisible(false); expect(viewArray.visibleEntries.length).toEqual(0); }); - it("shows additional entries if given true", function() { - spyOn(viewArray, "showAdditionalEntries_").and.returnValue(); + it("shows additional entries if given true", () => { + spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); viewArray.setVisible(true); expect(viewArray.showAdditionalEntries_).toHaveBeenCalled(); }); - it("does not show additional entries if given false", function() { - spyOn(viewArray, "showAdditionalEntries_").and.returnValue(); + it("does not show additional entries if given false", () => { + spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); viewArray.setVisible(false); expect(viewArray.showAdditionalEntries_).not.toHaveBeenCalled(); }); - it("starts timer if given true", function() { - spyOn(viewArray, "startTimer_").and.returnValue(); + it("starts timer if given true", () => { + spyOn(viewArray, "startTimer_").and.returnValue(null); viewArray.setVisible(true); expect(viewArray.startTimer_).toHaveBeenCalled(); }); - it("stops timer if given false", function() { - spyOn(viewArray, "stopTimer_").and.returnValue(); + it("stops timer if given false", () => { + spyOn(viewArray, "stopTimer_").and.returnValue(null); viewArray.setVisible(true); expect(viewArray.stopTimer_).toHaveBeenCalled(); diff --git a/static/js/services/angular-view-array/angular-view-array.ts b/static/js/services/angular-view-array/angular-view-array.ts new file mode 100644 index 000000000..1a8fe7a73 --- /dev/null +++ b/static/js/services/angular-view-array/angular-view-array.ts @@ -0,0 +1,108 @@ +import * as angular from 'angular'; +import { ViewArray } from './view-array'; +import { ViewArrayImpl } from './view-array.impl'; + + +/** + * Specialized wrapper around array which provides a toggle() method for viewing the contents of the + * array in a manner that is asynchronously filled in over a short time period. This prevents long + * pauses in the UI for ngRepeat's when the array is significant in size. + */ +angular + .module('quay') + .factory('AngularViewArray', AngularViewArrayFactory); + +AngularViewArrayFactory.$inject = [ + '$interval' +]; + +export default function AngularViewArrayFactory($interval) { + var ADDTIONAL_COUNT = 20; + + function _ViewArray() { + this.isVisible = false; + this.visibleEntries = null; + this.hasEntries = false; + this.entries = []; + this.hasHiddenEntries = false; + + this.timerRef_ = null; + this.currentIndex_ = 0; + } + + _ViewArray.prototype.length = function(): number { + return this.entries.length; + }; + + _ViewArray.prototype.get = function(index: number): any { + return this.entries[index]; + }; + + _ViewArray.prototype.push = function(elem: any): void { + this.entries.push(elem); + this.hasEntries = true; + + if (this.isVisible) { + this.startTimer_(); + } + }; + + _ViewArray.prototype.toggle = function(): void { + this.setVisible(!this.isVisible); + }; + + _ViewArray.prototype.setVisible = function(newState: boolean): void { + this.isVisible = newState; + + this.visibleEntries = []; + this.currentIndex_ = 0; + + if (newState) { + this.showAdditionalEntries_(); + this.startTimer_(); + } + else { + this.stopTimer_(); + } + }; + + _ViewArray.prototype.showAdditionalEntries_ = function(): void { + var i: number = 0; + for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) { + this.visibleEntries.push(this.entries[i]); + } + + this.currentIndex_ = i; + this.hasHiddenEntries = this.currentIndex_ < this.entries.length; + if (this.currentIndex_ >= this.entries.length) { + this.stopTimer_(); + } + }; + + _ViewArray.prototype.startTimer_ = function(): void { + if (this.timerRef_) { + return; + } + + var that = this; + this.timerRef_ = $interval(function() { + that.showAdditionalEntries_(); + }, 10); + }; + + _ViewArray.prototype.stopTimer_ = function(): void { + if (this.timerRef_) { + $interval.cancel(this.timerRef_); + this.timerRef_ = null; + } + }; + + var service: any = { + create: function(): any { + return new _ViewArray(); + } + }; + + return service; +} + diff --git a/static/js/services/angular-view-array/view-array.impl.ts b/static/js/services/angular-view-array/view-array.impl.ts new file mode 100644 index 000000000..a0050bc6a --- /dev/null +++ b/static/js/services/angular-view-array/view-array.impl.ts @@ -0,0 +1,90 @@ +import { ViewArray } from './view-array'; + + +export class ViewArrayImpl implements ViewArray { + + isVisible: boolean; + visibleEntries: any[]; + hasEntries: boolean; + entries: any[]; + hasHiddenEntries: boolean; + timerRef_: any; + currentIndex_: number; + + constructor(private interval: any, private additionalCount: number) { + this.isVisible = false; + this.visibleEntries = null; + this.hasEntries = false; + this.entries = []; + this.hasHiddenEntries = false; + this.timerRef_ = null; + this.currentIndex_ = 0; + } + + public length(): number { + return this.entries.length; + } + + public get(index: number): any { + return this.entries[index]; + } + + public push(elem: any): void { + this.entries.push(elem); + this.hasEntries = true; + + if (this.isVisible) { + this.startTimer_(); + } + } + + public toggle(): void { + this.setVisible(!this.isVisible); + } + + public setVisible(newState: boolean): void { + this.isVisible = newState; + + this.visibleEntries = []; + this.currentIndex_ = 0; + + if (newState) { + this.showAdditionalEntries_(); + this.startTimer_(); + } + else { + this.stopTimer_(); + } + } + + private showAdditionalEntries_(): void { + var i: number = 0; + for (i = this.currentIndex_; i < (this.currentIndex_ + this.additionalCount) && i < this.entries.length; ++i) { + this.visibleEntries.push(this.entries[i]); + } + + this.currentIndex_ = i; + this.hasHiddenEntries = this.currentIndex_ < this.entries.length; + if (this.currentIndex_ >= this.entries.length) { + this.stopTimer_(); + } + } + + private startTimer_(): void { + if (this.timerRef_) { + return; + } + + var that = this; + this.timerRef_ = this.interval(function() { + that.showAdditionalEntries_(); + }, 10); + } + + private stopTimer_(): void { + if (this.timerRef_) { + this.interval.cancel(this.timerRef_); + this.timerRef_ = null; + } + } +} \ No newline at end of file diff --git a/static/js/services/angular-view-array/view-array.ts b/static/js/services/angular-view-array/view-array.ts new file mode 100644 index 000000000..343ca1efb --- /dev/null +++ b/static/js/services/angular-view-array/view-array.ts @@ -0,0 +1,12 @@ +export abstract class ViewArray { + + public abstract length(): number; + + public abstract get(index: number): any; + + public abstract push(elem: any): void; + + public abstract toggle(): void; + + public abstract setVisible(newState: boolean): void; +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index b5029b6d5..42b13ae06 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/app.tsx"], + entry: ["./static/js/app.tsx", "./static/js/services/angular-view-array/angular-view-array.ts"], output: { path: path.resolve(__dirname, "static/js/build"), filename: "bundle.js" @@ -20,13 +20,14 @@ var config = { loader: "ts-loader", exclude: /node_modules/ }, - { - test: /\.scss$/, + { + test: /\.scss$/, loaders: ['style', 'css', 'sass'], - exclude: /node_modules/ + exclude: /node_modules/ } ] - } + }, + devtool: "cheap-eval-source-map", }; module.exports = config; From b24d3b952b306cb328256fd54ee586d3c6140c67 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 18 Jan 2017 17:14:57 -0800 Subject: [PATCH 02/19] getting error: a dependency to an entry point is not allowed --- karma.conf.js | 1 + .../angular-view-array.spec.ts | 1 + .../angular-view-array/angular-view-array.ts | 90 +------------------ 3 files changed, 6 insertions(+), 86 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 73149f50f..ed75118d7 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -43,6 +43,7 @@ module.exports = function(config) { 'static/js/**/*.ts': ['webpack'], }, webpack: { + resolve: webpackConfig.resolve, module: webpackConfig.module, }, webpackMiddleware: { diff --git a/static/js/services/angular-view-array/angular-view-array.spec.ts b/static/js/services/angular-view-array/angular-view-array.spec.ts index ce30f9268..2ec39ea7a 100644 --- a/static/js/services/angular-view-array/angular-view-array.spec.ts +++ b/static/js/services/angular-view-array/angular-view-array.spec.ts @@ -1,4 +1,5 @@ import IInjectorService = angular.auto.IInjectorService; +// import { ViewArrayImpl } from './view-array.impl'; describe("Service: AngularViewArray", () => { diff --git a/static/js/services/angular-view-array/angular-view-array.ts b/static/js/services/angular-view-array/angular-view-array.ts index 1a8fe7a73..795fb7e77 100644 --- a/static/js/services/angular-view-array/angular-view-array.ts +++ b/static/js/services/angular-view-array/angular-view-array.ts @@ -16,93 +16,11 @@ AngularViewArrayFactory.$inject = [ '$interval' ]; -export default function AngularViewArrayFactory($interval) { - var ADDTIONAL_COUNT = 20; - - function _ViewArray() { - this.isVisible = false; - this.visibleEntries = null; - this.hasEntries = false; - this.entries = []; - this.hasHiddenEntries = false; - - this.timerRef_ = null; - this.currentIndex_ = 0; - } - - _ViewArray.prototype.length = function(): number { - return this.entries.length; - }; - - _ViewArray.prototype.get = function(index: number): any { - return this.entries[index]; - }; - - _ViewArray.prototype.push = function(elem: any): void { - this.entries.push(elem); - this.hasEntries = true; - - if (this.isVisible) { - this.startTimer_(); +export default function AngularViewArrayFactory($interval): any { + return { + create: function(): ViewArray { + return new ViewArrayImpl($interval, 20); } }; - - _ViewArray.prototype.toggle = function(): void { - this.setVisible(!this.isVisible); - }; - - _ViewArray.prototype.setVisible = function(newState: boolean): void { - this.isVisible = newState; - - this.visibleEntries = []; - this.currentIndex_ = 0; - - if (newState) { - this.showAdditionalEntries_(); - this.startTimer_(); - } - else { - this.stopTimer_(); - } - }; - - _ViewArray.prototype.showAdditionalEntries_ = function(): void { - var i: number = 0; - for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) { - this.visibleEntries.push(this.entries[i]); - } - - this.currentIndex_ = i; - this.hasHiddenEntries = this.currentIndex_ < this.entries.length; - if (this.currentIndex_ >= this.entries.length) { - this.stopTimer_(); - } - }; - - _ViewArray.prototype.startTimer_ = function(): void { - if (this.timerRef_) { - return; - } - - var that = this; - this.timerRef_ = $interval(function() { - that.showAdditionalEntries_(); - }, 10); - }; - - _ViewArray.prototype.stopTimer_ = function(): void { - if (this.timerRef_) { - $interval.cancel(this.timerRef_); - this.timerRef_ = null; - } - }; - - var service: any = { - create: function(): any { - return new _ViewArray(); - } - }; - - return service; } From d5a74af024002cbb3b445c12fc4df97c0645d530 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 18 Jan 2017 22:10:43 -0800 Subject: [PATCH 03/19] typescript tests working --- karma.conf.js | 2 +- .../angular-view-array/angular-view-array.spec.ts | 15 +++++++++------ .../angular-view-array/angular-view-array.ts | 10 ++++++---- .../angular-view-array/view-array.impl.ts | 6 +++--- tsconfig.json | 1 + 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index ed75118d7..3889075c9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -29,10 +29,10 @@ module.exports = function(config) { // Application resources 'static/js/**/*.js', - 'static/js/**/*.ts', // Tests 'static/test/**/*.js', + 'static/js/**/*spec.ts', ], exclude: [ 'static/js/build/bundle.js', diff --git a/static/js/services/angular-view-array/angular-view-array.spec.ts b/static/js/services/angular-view-array/angular-view-array.spec.ts index 2ec39ea7a..4afc88184 100644 --- a/static/js/services/angular-view-array/angular-view-array.spec.ts +++ b/static/js/services/angular-view-array/angular-view-array.spec.ts @@ -1,26 +1,29 @@ -import IInjectorService = angular.auto.IInjectorService; -// import { ViewArrayImpl } from './view-array.impl'; +import { angularViewArrayFactory } from './angular-view-array'; +import { ViewArrayImpl } from './view-array.impl'; +import * as angular from 'angular'; describe("Service: AngularViewArray", () => { var angularViewArray: any; + var $interval: ng.IIntervalService; beforeEach(angular.mock.module('quay')); - beforeEach(inject(($injector: IInjectorService) => { - angularViewArray = $injector.get('AngularViewArray'); + beforeEach(inject(($injector: ng.auto.IInjectorService) => { + $interval = $injector.get('$interval'); + angularViewArray = angularViewArrayFactory($interval); })); describe("create", () => { it("returns a ViewArray object", () => { - var viewArray: any = angularViewArray.create(); + var viewArray: ViewArrayImpl = angularViewArray.create(); expect(viewArray).toBeDefined(); }); describe("returned ViewArray object", () => { - var viewArray: any; + var viewArray: ViewArrayImpl; beforeEach(() => { viewArray = angularViewArray.create(); diff --git a/static/js/services/angular-view-array/angular-view-array.ts b/static/js/services/angular-view-array/angular-view-array.ts index 795fb7e77..56c1c6b6f 100644 --- a/static/js/services/angular-view-array/angular-view-array.ts +++ b/static/js/services/angular-view-array/angular-view-array.ts @@ -10,16 +10,18 @@ import { ViewArrayImpl } from './view-array.impl'; */ angular .module('quay') - .factory('AngularViewArray', AngularViewArrayFactory); + .factory('AngularViewArray', angularViewArrayFactory); -AngularViewArrayFactory.$inject = [ +angularViewArrayFactory.$inject = [ '$interval' ]; -export default function AngularViewArrayFactory($interval): any { +export function angularViewArrayFactory($interval): any { + const ADDITIONAL_ENTRIES: number = 20; + return { create: function(): ViewArray { - return new ViewArrayImpl($interval, 20); + return new ViewArrayImpl($interval, ADDITIONAL_ENTRIES); } }; } diff --git a/static/js/services/angular-view-array/view-array.impl.ts b/static/js/services/angular-view-array/view-array.impl.ts index a0050bc6a..b45ce185a 100644 --- a/static/js/services/angular-view-array/view-array.impl.ts +++ b/static/js/services/angular-view-array/view-array.impl.ts @@ -57,7 +57,7 @@ export class ViewArrayImpl implements ViewArray { } } - private showAdditionalEntries_(): void { + public showAdditionalEntries_(): void { var i: number = 0; for (i = this.currentIndex_; i < (this.currentIndex_ + this.additionalCount) && i < this.entries.length; ++i) { this.visibleEntries.push(this.entries[i]); @@ -70,7 +70,7 @@ export class ViewArrayImpl implements ViewArray { } } - private startTimer_(): void { + public startTimer_(): void { if (this.timerRef_) { return; } @@ -81,7 +81,7 @@ export class ViewArrayImpl implements ViewArray { }, 10); } - private stopTimer_(): void { + public stopTimer_(): void { if (this.timerRef_) { this.interval.cancel(this.timerRef_); this.timerRef_ = null; diff --git a/tsconfig.json b/tsconfig.json index 39b63f61b..d8acb9b2a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "baseUrl": ".", "jsx": "react", "module": "commonjs", + "moduleResolution": "node", "outDir": "./build/", "target": "es5", "sourceMap": true, From c55e9f2d127a63371b015778cbace6ce98dc5e03 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 19 Jan 2017 00:53:38 -0800 Subject: [PATCH 04/19] moved modules to typescript. Tell Webpack to use global window.angular object in order to migrate components gradually into bundle --- external_libraries.py | 2 +- karma.conf.js | 17 +- package.json | 2 +- static/js/app.js | 353 ------------------ .../components/pages/repo-page/main.tsx | 8 +- static/js/directives/object-order-by.js | 4 +- static/js/quay-pages.module.ts | 34 ++ static/js/quay.config.ts | 201 ++++++++++ static/js/quay.module.ts | 53 +++ static/js/quay.run.ts | 127 +++++++ .../angular-view-array.spec.ts | 263 ++++++------- .../angular-view-array/angular-view-array.ts | 4 - .../angular-view-array/view-array.impl.ts | 14 +- templates/base.html | 2 + webpack.config.js | 11 +- 15 files changed, 590 insertions(+), 505 deletions(-) create mode 100644 static/js/quay-pages.module.ts create mode 100644 static/js/quay.config.ts create mode 100644 static/js/quay.module.ts create mode 100644 static/js/quay.run.ts diff --git a/external_libraries.py b/external_libraries.py index d99431283..28d697376 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -7,7 +7,7 @@ LOCAL_DIRECTORY = '/static/ldn/' EXTERNAL_JS = [ 'code.jquery.com/jquery.js', 'netdna.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js', - 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js', + 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-route.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-sanitize.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-animate.min.js', diff --git a/karma.conf.js b/karma.conf.js index 3889075c9..43df9a4dd 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -28,6 +28,7 @@ module.exports = function(config) { 'static/lib/**/*.js', // Application resources + 'static/js/quay.module.ts', 'static/js/**/*.js', // Tests @@ -44,7 +45,21 @@ module.exports = function(config) { }, webpack: { resolve: webpackConfig.resolve, - module: webpackConfig.module, + externals: webpackConfig.externals, + module: { + loaders: [ + { + test: /\.tsx?$/, + loader: "ts-loader", + exclude: /node_modules/ + }, + { + test: /\.scss$/, + loaders: ['style', 'css', 'sass'], + exclude: /node_modules/ + }, + ] + } }, webpackMiddleware: { stats: 'errors-only' diff --git a/package.json b/package.json index 4d87b1c97..42ccf8400 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "version": "1.0.0", "scripts": { - "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS", + "test": "./node_modules/.bin/karma start --single-run --browsers Chrome", "build": "./node_modules/.bin/webpack --progress -p -v", "watch": "./node_modules/.bin/webpack --watch" }, diff --git a/static/js/app.js b/static/js/app.js index fd163f262..f0fa0c1a3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,356 +1,3 @@ var TEAM_PATTERN = '^[a-z][a-z0-9]+$'; var ROBOT_PATTERN = '^[a-z][a-z0-9_]{3,29}$'; var USERNAME_PATTERN = '^(?=.{4,30}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$'; - -// Define the pages module. -quayPages = angular.module('quayPages', [], function(){}); - -// Define a constant for creating pages. -quayPages.constant('pages', { - '_pages': {}, - - 'create': function(pageName, templateName, opt_controller, opt_flags, opt_profiles) { - var profiles = opt_profiles || ['old-layout', 'layout']; - for (var i = 0; i < profiles.length; ++i) { - this._pages[profiles[i] + ':' + pageName] = { - 'name': pageName, - 'controller': opt_controller, - 'templateName': templateName, - 'flags': opt_flags || {} - }; - } - }, - - 'get': function(pageName, profiles) { - for (var i = 0; i < profiles.length; ++i) { - var current = profiles[i]; - var key = current.id + ':' + pageName; - var page = this._pages[key]; - if (page) { - return [current, page]; - } - } - - return null; - } -}); - -quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'cfp.hotkeys', 'angular-tour', 'restangular', 'angularMoment', - 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', - 'core-ui', 'core-config-setup', 'quayPages', 'infinite-scroll', 'react']; - -if (window.__config && (window.__config.MIXPANEL_KEY || window.__config.MUNCHKIN_KEY || window.__config.GOOGLE_ANALYTICS_KEY)) { - quayDependencies.push('angulartics'); -} - -if (window.__config && window.__config.MIXPANEL_KEY) { - quayDependencies.push('angulartics.mixpanel'); -} - -if (window.__config && window.__config.MUNCHKIN_KEY) { - quayDependencies.push('angulartics.marketo'); -} - -if (window.__config && window.__config.GOOGLE_ANALYTICS_KEY) { - quayDependencies.push('angulartics.google.analytics'); -} - -if (window.__config && window.__config.RECAPTCHA_SITE_KEY) { - quayDependencies.push('vcRecaptcha'); -} - -// Define the application. -quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) { - cfpLoadingBarProvider.includeSpinner = false; -}); - -// Disable tooltips on touch devices. -quayApp.config(['$tooltipProvider', function ($tooltipProvider) { - var tooltipFactory = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; - - // decorate the tooltip getter - $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window) { - if ('ontouchstart' in $window) { - var existing = 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.attr('bs-tooltip') == null) { - return existing.apply(this, arguments); - } else { - return null; - } - }; - } - - return tooltipFactory.apply(this, arguments); - }; -}]); - -quayApp.config(['$compileProvider', function ($compileProvider) { - if (!window.__config['DEBUG']) { - $compileProvider.debugInfoEnabled(false); - } -}]); - -// Configure the routes. -quayApp.config(['$routeProvider', '$locationProvider', 'pages', 'RouteBuilderProvider', - function($routeProvider, $locationProvider, pages, RouteBuilderProvider) { - $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 layoutProfile = 'layout'; - window.console.log('Using layout profile: ' + layoutProfile); - - var routeBuilder = new RouteBuilderProvider.$get()($routeProvider, pages, [ - // Start with the old pages (if we asked for it). - {id: 'old-layout', templatePath: '/static/partials/'}, - - // Fallback back combined new/existing pages. - {id: 'layout', templatePath: '/static/partials/'} - ], layoutProfile); - - if (window.__features.SUPER_USERS) { - // QE Management - routeBuilder.route('/superuser/', 'superuser') - - // QE Setup - .route('/setup/', 'setup'); - } - - routeBuilder - // 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') - - // 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') - - // Enterprise marketing page - .route('/enterprise', 'enterprise') - - // 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'); -}]); - -// Configure compile provider to add additional URL prefixes to the sanitization list. We use -// these on the Contact page. -quayApp.config(function($compileProvider) { - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); -}); - -// Configure the API provider. -quayApp.config(function(RestangularProvider) { - RestangularProvider.setBaseUrl('/api/v1/'); -}); - -// Configure analytics. -if (window.__config && window.__config.MIXPANEL_KEY) { - quayApp.config(['$analyticsProvider', function($analyticsProvider) { - $analyticsProvider.virtualPageviews(true); - }]); -} - -// Configure sentry. -if (window.__config && window.__config.SENTRY_PUBLIC_DSN) { - quayApp.config(function($provide) { - $provide.decorator("$exceptionHandler", function($delegate) { - return function(ex, cause) { - $delegate(ex, cause); - Raven.captureException(ex, {extra: {cause: cause}}); - }; - }); - }); -} - -// Run the application. -quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService', 'MetaService', 'UIService', - function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService, MetaService, UIService) { - - var defaultTitle = window.__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.status == 503) { - $('#cannotContactService').modal({}); - return false; - } - - if (response.status == 500) { - document.location = '/500'; - return false; - } - - if (!response.data) { - return true; - } - - var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; - if (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) { - 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; - if ($rootScope.title != title) { - $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 = false; - window.__isLoading = function() { - if (!initallyChecked) { - initallyChecked = true; - return true; - } - return $http.pendingRequests.length > 0; - }; -}]); diff --git a/static/js/directives/components/pages/repo-page/main.tsx b/static/js/directives/components/pages/repo-page/main.tsx index 5dd13c624..7fb8e0453 100644 --- a/static/js/directives/components/pages/repo-page/main.tsx +++ b/static/js/directives/components/pages/repo-page/main.tsx @@ -1,22 +1,22 @@ import "sass/repo-page/repo-page.scss"; import * as angular from "angular"; +import quayPages from '../../../../quay-pages.module'; import repoHeader from "./header"; import repoSidebar from "./sidebar"; import repoBody from "./body"; export function rpDirectives(){ - angular.module('quayPages').directive('rpHeader', function(reactDirective) { + angular.module(quayPages).directive('rpHeader', function(reactDirective) { return reactDirective(repoHeader); }); - angular.module('quayPages').directive('rpSidebar', function(reactDirective) { + angular.module(quayPages).directive('rpSidebar', function(reactDirective) { return reactDirective(repoSidebar); }); - angular.module('quayPages').directive('rpBody', function(reactDirective, ApiService) { + angular.module(quayPages).directive('rpBody', function(reactDirective, ApiService) { return reactDirective(repoBody, undefined, {}, {api: ApiService}); }); } - \ No newline at end of file diff --git a/static/js/directives/object-order-by.js b/static/js/directives/object-order-by.js index 23f9dc096..716781e85 100644 --- a/static/js/directives/object-order-by.js +++ b/static/js/directives/object-order-by.js @@ -1,5 +1,7 @@ // From: http://justinklemm.com/angularjs-filter-ordering-objects-ngrepeat/ under MIT License -quayApp.filter('orderObjectBy', function() { +angular + .module('quay') + .filter('orderObjectBy', function() { return function(items, field, reverse) { var filtered = []; angular.forEach(items, function(item) { diff --git a/static/js/quay-pages.module.ts b/static/js/quay-pages.module.ts new file mode 100644 index 000000000..7a203e984 --- /dev/null +++ b/static/js/quay-pages.module.ts @@ -0,0 +1,34 @@ +import * as angular from 'angular'; + + +export default angular + .module('quayPages', []) + .constant('pages', { + '_pages': {}, + + 'create': function(pageName, templateName, opt_controller, opt_flags, opt_profiles) { + var profiles = opt_profiles || ['old-layout', 'layout']; + for (var i = 0; i < profiles.length; ++i) { + this._pages[profiles[i] + ':' + pageName] = { + 'name': pageName, + 'controller': opt_controller, + 'templateName': templateName, + 'flags': opt_flags || {} + }; + } + }, + + 'get': function(pageName, profiles) { + for (var i = 0; i < profiles.length; ++i) { + var current = profiles[i]; + var key = current.id + ':' + pageName; + var page = this._pages[key]; + if (page) { + return [current, page]; + } + } + + return null; + } + }) + .name; \ No newline at end of file diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts new file mode 100644 index 000000000..cf280ff11 --- /dev/null +++ b/static/js/quay.config.ts @@ -0,0 +1,201 @@ +import * as Raven from 'raven-js'; + + +quayConfig.$inject = [ + '$provide', + 'cfpLoadingBarProvider', + '$tooltipProvider', + '$compileProvider', + '$routeProvider', + '$locationProvider', + 'pages', + 'RouteBuilderProvider', + 'RestangularProvider', + '$analyticsProvider', +]; + +export function quayConfig( + $provide, + cfpLoadingBarProvider, + $tooltipProvider, + $compileProvider, + $routeProvider, + $locationProvider, + pages, + RouteBuilderProvider, + RestangularProvider, + $analyticsProvider) { + cfpLoadingBarProvider.includeSpinner = false; + + // decorate the tooltip getter + var tooltipFactory = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; + $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window) { + if ('ontouchstart' in $window) { + var existing = 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.attr('bs-tooltip') == null) { + return existing.apply(this, arguments); + } else { + return null; + } + }; + } + + return tooltipFactory.apply(this, arguments); + }; + + if (!(window).__config['DEBUG']) { + $compileProvider.debugInfoEnabled(false); + } + + $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 layoutProfile = 'layout'; + console.log('Using layout profile: ' + layoutProfile); + + var routeBuilder = new RouteBuilderProvider.$get()($routeProvider, pages, [ + // Start with the old pages (if we asked for it). + {id: 'old-layout', templatePath: '/static/partials/'}, + + // Fallback back combined new/existing pages. + {id: 'layout', templatePath: '/static/partials/'} + ], layoutProfile); + + if ((window).__features.SUPER_USERS) { + // QE Management + routeBuilder.route('/superuser/', 'superuser') + + // QE Setup + .route('/setup/', 'setup'); + } + + routeBuilder + // 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') + + // 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') + + // Enterprise marketing page + .route('/enterprise', 'enterprise') + + // 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'); + + // 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 ((window).__config && (window).__config.MIXPANEL_KEY) { + $analyticsProvider.virtualPageviews(true); + } + + // Configure sentry. + if ((window).__config && (window).__config.SENTRY_PUBLIC_DSN) { + $provide.decorator("$exceptionHandler", function($delegate) { + return function(ex, cause) { + $delegate(ex, cause); + Raven.captureException(ex, {extra: {cause: cause}}); + }; + }); + } +} \ No newline at end of file diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts new file mode 100644 index 000000000..7f42e3baf --- /dev/null +++ b/static/js/quay.module.ts @@ -0,0 +1,53 @@ +import * as angular from 'angular'; +import { quayConfig } from './quay.config.ts'; +import quayPages from './quay-pages.module'; +import quayRun from './quay.run'; +import { angularViewArrayFactory } from './services/angular-view-array/angular-view-array'; + + +var quayDependencies: string[] = [ + quayPages, + 'ngRoute', + '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 ((window).__config && ((window).__config.MIXPANEL_KEY || (window).__config.MUNCHKIN_KEY || (window).__config.GOOGLE_ANALYTICS_KEY)) { + quayDependencies.push('angulartics'); +} + +if ((window).__config && (window).__config.MIXPANEL_KEY) { + quayDependencies.push('angulartics.mixpanel'); +} + +if ((window).__config && (window).__config.MUNCHKIN_KEY) { + quayDependencies.push('angulartics.marketo'); +} + +if ((window).__config && (window).__config.GOOGLE_ANALYTICS_KEY) { + quayDependencies.push('angulartics.google.analytics'); +} + +if ((window).__config && (window).__config.RECAPTCHA_SITE_KEY) { + quayDependencies.push('vcRecaptcha'); +} + +export default angular + .module('quay', quayDependencies) + .config(quayConfig) + .factory('AngularViewArray', angularViewArrayFactory) + .run(quayRun) + .name; \ 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..a8a2a139f --- /dev/null +++ b/static/js/quay.run.ts @@ -0,0 +1,127 @@ +import * as $ from 'jquery'; + + +quayRun.$inject = [ + '$location', + '$rootScope', + 'Restangular', + 'UserService', + 'PlanService', + '$http', + '$timeout', + 'CookieService', + 'Features', + '$anchorScroll', + 'UtilService', + 'MetaService', +]; + +export default function quayRun( + $location, + $rootScope, + Restangular, + UserService, + PlanService, + $http, + $timeout, + CookieService, + Features, + $anchorScroll, + UtilService, + MetaService) { + var defaultTitle = (window).__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.status == 503) { + ($('#cannotContactService')).modal({}); + return false; + } + + if (response.status == 500) { + window.location.href = '/500'; + return false; + } + + if (!response.data) { + return true; + } + + var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; + if (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) { + 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; + if ($rootScope.title != title) { + $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 = false; + (window).__isLoading = function() { + if (!initallyChecked) { + initallyChecked = true; + return true; + } + return $http.pendingRequests.length > 0; + }; +} \ No newline at end of file diff --git a/static/js/services/angular-view-array/angular-view-array.spec.ts b/static/js/services/angular-view-array/angular-view-array.spec.ts index 4afc88184..2a6e903fc 100644 --- a/static/js/services/angular-view-array/angular-view-array.spec.ts +++ b/static/js/services/angular-view-array/angular-view-array.spec.ts @@ -1,14 +1,11 @@ import { angularViewArrayFactory } from './angular-view-array'; import { ViewArrayImpl } from './view-array.impl'; -import * as angular from 'angular'; describe("Service: AngularViewArray", () => { var angularViewArray: any; var $interval: ng.IIntervalService; - beforeEach(angular.mock.module('quay')); - beforeEach(inject(($injector: ng.auto.IInjectorService) => { $interval = $injector.get('$interval'); angularViewArray = angularViewArrayFactory($interval); @@ -16,136 +13,140 @@ describe("Service: AngularViewArray", () => { describe("create", () => { - it("returns a ViewArray object", () => { - var viewArray: ViewArrayImpl = angularViewArray.create(); - - expect(viewArray).toBeDefined(); + it("sanity", () => { + expect(angularViewArrayFactory).toBeDefined(); }); - describe("returned ViewArray object", () => { - var viewArray: ViewArrayImpl; + // it("returns a ViewArray object", () => { + // var viewArray: ViewArrayImpl = angularViewArray.create(); + // + // expect(viewArray).toBeDefined(); + // }); - beforeEach(() => { - viewArray = angularViewArray.create(); - }); - - describe("constructor", () => { - // TODO - }); - - describe("length", () => { - - it("returns the number of entries", () => { - viewArray.entries = [{}, {}, {}]; - - expect(viewArray.length()).toEqual(viewArray.entries.length); - }); - }); - - describe("get", () => { - - it("returns the entry at a given index", () => { - var index: number = 8; - viewArray.entries = new Array(10); - viewArray.entries[index] = 3; - - expect(viewArray.get(index)).toEqual(viewArray.entries[index]); - }); - }); - - describe("push", () => { - - it("adds given element to the end of entries", () => { - var element: number = 3; - var originalLength: number = viewArray.length(); - viewArray.push(element); - - expect(viewArray.entries.length).toEqual(originalLength + 1); - expect(viewArray.get(originalLength)).toEqual(element); - }); - - it("sets 'hasEntries' to true", () => { - viewArray.push(2); - - expect(viewArray.hasEntries).toBe(true); - }); - - it("starts timer if 'isVisible' is true", () => { - spyOn(viewArray, "startTimer_").and.returnValue(null); - viewArray.isVisible = true; - viewArray.push(2); - - expect(viewArray.startTimer_).toHaveBeenCalled(); - }); - - it("does not start timer if 'isVisible' is false", () => { - spyOn(viewArray, "startTimer_").and.returnValue(null); - viewArray.isVisible = false; - viewArray.push(2); - - expect(viewArray.startTimer_).not.toHaveBeenCalled(); - }); - }); - - describe("toggle", () => { - - it("sets 'isVisible' to false if currently true", () => { - viewArray.isVisible = true; - viewArray.toggle(); - - expect(viewArray.isVisible).toBe(false); - }); - - it("sets 'isVisible' to true if currently false", () => { - viewArray.isVisible = false; - viewArray.toggle(); - - expect(viewArray.isVisible).toBe(true); - }); - }); - - describe("setVisible", () => { - - it("sets 'isVisible' to false if given false", () => { - viewArray.setVisible(false); - - expect(viewArray.isVisible).toBe(false); - }); - - it("sets 'visibleEntries' to empty array if given false", () => { - viewArray.setVisible(false); - - expect(viewArray.visibleEntries.length).toEqual(0); - }); - - it("shows additional entries if given true", () => { - spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); - viewArray.setVisible(true); - - expect(viewArray.showAdditionalEntries_).toHaveBeenCalled(); - }); - - it("does not show additional entries if given false", () => { - spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); - viewArray.setVisible(false); - - expect(viewArray.showAdditionalEntries_).not.toHaveBeenCalled(); - }); - - it("starts timer if given true", () => { - spyOn(viewArray, "startTimer_").and.returnValue(null); - viewArray.setVisible(true); - - expect(viewArray.startTimer_).toHaveBeenCalled(); - }); - - it("stops timer if given false", () => { - spyOn(viewArray, "stopTimer_").and.returnValue(null); - viewArray.setVisible(true); - - expect(viewArray.stopTimer_).toHaveBeenCalled(); - }); - }); - }); + // describe("returned ViewArray object", () => { + // var viewArray: ViewArrayImpl; + // + // beforeEach(() => { + // viewArray = angularViewArray.create(); + // }); + // + // describe("constructor", () => { + // // TODO + // }); + // + // describe("length", () => { + // + // it("returns the number of entries", () => { + // viewArray.entries = [{}, {}, {}]; + // + // expect(viewArray.length()).toEqual(viewArray.entries.length); + // }); + // }); + // + // describe("get", () => { + // + // it("returns the entry at a given index", () => { + // var index: number = 8; + // viewArray.entries = new Array(10); + // viewArray.entries[index] = 3; + // + // expect(viewArray.get(index)).toEqual(viewArray.entries[index]); + // }); + // }); + // + // describe("push", () => { + // + // it("adds given element to the end of entries", () => { + // var element: number = 3; + // var originalLength: number = viewArray.length(); + // viewArray.push(element); + // + // expect(viewArray.entries.length).toEqual(originalLength + 1); + // expect(viewArray.get(originalLength)).toEqual(element); + // }); + // + // it("sets 'hasEntries' to true", () => { + // viewArray.push(2); + // + // expect(viewArray.hasEntries).toBe(true); + // }); + // + // it("starts timer if 'isVisible' is true", () => { + // spyOn(viewArray, "startTimer_").and.returnValue(null); + // viewArray.isVisible = true; + // viewArray.push(2); + // + // expect(viewArray.startTimer_).toHaveBeenCalled(); + // }); + // + // it("does not start timer if 'isVisible' is false", () => { + // spyOn(viewArray, "startTimer_").and.returnValue(null); + // viewArray.isVisible = false; + // viewArray.push(2); + // + // expect(viewArray.startTimer_).not.toHaveBeenCalled(); + // }); + // }); + // + // describe("toggle", () => { + // + // it("sets 'isVisible' to false if currently true", () => { + // viewArray.isVisible = true; + // viewArray.toggle(); + // + // expect(viewArray.isVisible).toBe(false); + // }); + // + // it("sets 'isVisible' to true if currently false", () => { + // viewArray.isVisible = false; + // viewArray.toggle(); + // + // expect(viewArray.isVisible).toBe(true); + // }); + // }); + // + // describe("setVisible", () => { + // + // it("sets 'isVisible' to false if given false", () => { + // viewArray.setVisible(false); + // + // expect(viewArray.isVisible).toBe(false); + // }); + // + // it("sets 'visibleEntries' to empty array if given false", () => { + // viewArray.setVisible(false); + // + // expect(viewArray.visibleEntries.length).toEqual(0); + // }); + // + // it("shows additional entries if given true", () => { + // spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); + // viewArray.setVisible(true); + // + // expect(viewArray.showAdditionalEntries_).toHaveBeenCalled(); + // }); + // + // it("does not show additional entries if given false", () => { + // spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); + // viewArray.setVisible(false); + // + // expect(viewArray.showAdditionalEntries_).not.toHaveBeenCalled(); + // }); + // + // it("starts timer if given true", () => { + // spyOn(viewArray, "startTimer_").and.returnValue(null); + // viewArray.setVisible(true); + // + // expect(viewArray.startTimer_).toHaveBeenCalled(); + // }); + // + // it("stops timer if given false", () => { + // spyOn(viewArray, "stopTimer_").and.returnValue(null); + // viewArray.setVisible(true); + // + // expect(viewArray.stopTimer_).toHaveBeenCalled(); + // }); + // }); + // }); }); }); \ No newline at end of file diff --git a/static/js/services/angular-view-array/angular-view-array.ts b/static/js/services/angular-view-array/angular-view-array.ts index 56c1c6b6f..0de564392 100644 --- a/static/js/services/angular-view-array/angular-view-array.ts +++ b/static/js/services/angular-view-array/angular-view-array.ts @@ -1,4 +1,3 @@ -import * as angular from 'angular'; import { ViewArray } from './view-array'; import { ViewArrayImpl } from './view-array.impl'; @@ -8,9 +7,6 @@ import { ViewArrayImpl } from './view-array.impl'; * array in a manner that is asynchronously filled in over a short time period. This prevents long * pauses in the UI for ngRepeat's when the array is significant in size. */ -angular - .module('quay') - .factory('AngularViewArray', angularViewArrayFactory); angularViewArrayFactory.$inject = [ '$interval' diff --git a/static/js/services/angular-view-array/view-array.impl.ts b/static/js/services/angular-view-array/view-array.impl.ts index b45ce185a..cca978307 100644 --- a/static/js/services/angular-view-array/view-array.impl.ts +++ b/static/js/services/angular-view-array/view-array.impl.ts @@ -3,13 +3,13 @@ import { ViewArray } from './view-array'; export class ViewArrayImpl implements ViewArray { - isVisible: boolean; - visibleEntries: any[]; - hasEntries: boolean; - entries: any[]; - hasHiddenEntries: boolean; - timerRef_: any; - currentIndex_: number; + public isVisible: boolean; + public visibleEntries: any[]; + public hasEntries: boolean; + public entries: any[]; + public hasHiddenEntries: boolean; + public timerRef_: any; + public currentIndex_: number; constructor(private interval: any, private additionalCount: number) { this.isVisible = false; diff --git a/templates/base.html b/templates/base.html index da1baddf3..e455ac609 100644 --- a/templates/base.html +++ b/templates/base.html @@ -64,6 +64,8 @@ {% endblock %} + + {% for script_path, cache_buster in main_scripts %} {% endfor %} diff --git a/webpack.config.js b/webpack.config.js index 42b13ae06..9f542185e 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/app.tsx", "./static/js/services/angular-view-array/angular-view-array.ts"], + entry: ["./static/js/app.tsx", "./static/js/quay.module.ts"], output: { path: path.resolve(__dirname, "static/js/build"), filename: "bundle.js" @@ -13,6 +13,9 @@ var config = { "sass": path.resolve('./static/css/directives/components/pages/') } }, + externals: { + "angular": "angular", + }, module: { loaders: [ { @@ -24,10 +27,14 @@ var config = { test: /\.scss$/, loaders: ['style', 'css', 'sass'], exclude: /node_modules/ + }, + { + test: /angular\.js$/, + loader: 'expose?angular', } ] }, - devtool: "cheap-eval-source-map", + devtool: "source-map", }; module.exports = config; From 8a9a2972ec1160c356babf6d5983e308b8aba14f Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 19 Jan 2017 01:48:15 -0800 Subject: [PATCH 05/19] moved naming patterns to seperate module and Angular constant --- karma.conf.js | 8 +- static/js/app.js | 3 - static/js/constants/name-patterns.constant.ts | 5 + .../js/directives/ui/create-robot-dialog.js | 4 +- static/js/directives/ui/create-team-dialog.js | 4 +- static/js/directives/ui/namespace-input.js | 4 +- static/js/quay.module.ts | 6 +- static/js/route-builder/route-builder.spec.js | 224 +++++++-------- .../angular-view-array.spec.ts | 260 +++++++++--------- 9 files changed, 262 insertions(+), 256 deletions(-) delete mode 100644 static/js/app.js create mode 100644 static/js/constants/name-patterns.constant.ts diff --git a/karma.conf.js b/karma.conf.js index 43df9a4dd..d0c92772a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -29,11 +29,12 @@ module.exports = function(config) { // Application resources 'static/js/quay.module.ts', - 'static/js/**/*.js', + // 'static/js/**/*.js', // Tests 'static/test/**/*.js', - 'static/js/**/*spec.ts', + 'static/js/**/*.spec.js', + 'static/js/**/*.spec.ts', ], exclude: [ 'static/js/build/bundle.js', @@ -41,7 +42,8 @@ module.exports = function(config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], - 'static/js/**/*.ts': ['webpack'], + 'static/js/quay.module.ts': ['webpack'], + 'static/js/**/*.spec.ts': ['webpack'], }, webpack: { resolve: webpackConfig.resolve, diff --git a/static/js/app.js b/static/js/app.js deleted file mode 100644 index f0fa0c1a3..000000000 --- a/static/js/app.js +++ /dev/null @@ -1,3 +0,0 @@ -var TEAM_PATTERN = '^[a-z][a-z0-9]+$'; -var ROBOT_PATTERN = '^[a-z][a-z0-9_]{3,29}$'; -var USERNAME_PATTERN = '^(?=.{4,30}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$'; diff --git a/static/js/constants/name-patterns.constant.ts b/static/js/constants/name-patterns.constant.ts new file mode 100644 index 000000000..3699175bc --- /dev/null +++ b/static/js/constants/name-patterns.constant.ts @@ -0,0 +1,5 @@ +export default { + TEAM_PATTERN: '^[a-z][a-z0-9]+$', + ROBOT_PATTERN: '^[a-z][a-z0-9_]{3,29}$', + USERNAME_PATTERN: '^(?=.{4,30}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$', +}; \ No newline at end of file diff --git a/static/js/directives/ui/create-robot-dialog.js b/static/js/directives/ui/create-robot-dialog.js index 59beecc90..53e18023a 100644 --- a/static/js/directives/ui/create-robot-dialog.js +++ b/static/js/directives/ui/create-robot-dialog.js @@ -12,8 +12,8 @@ angular.module('quay').directive('createRobotDialog', function () { 'info': '=info', 'robotCreated': '&robotCreated' }, - controller: function($scope, $element, ApiService, UserService) { - $scope.ROBOT_PATTERN = ROBOT_PATTERN; + controller: function($scope, $element, ApiService, UserService, namePatterns) { + $scope.ROBOT_PATTERN = namePatterns.ROBOT_PATTERN; $scope.robotFinished = function(robot) { $scope.robotCreated({'robot': robot}); diff --git a/static/js/directives/ui/create-team-dialog.js b/static/js/directives/ui/create-team-dialog.js index 4d63e57b8..49e71595b 100644 --- a/static/js/directives/ui/create-team-dialog.js +++ b/static/js/directives/ui/create-team-dialog.js @@ -12,8 +12,8 @@ angular.module('quay').directive('createTeamDialog', function () { 'info': '=info', 'teamCreated': '&teamCreated' }, - controller: function($scope, $element, ApiService, UserService) { - $scope.TEAM_PATTERN = TEAM_PATTERN; + controller: function($scope, $element, ApiService, UserService, namePatterns) { + $scope.TEAM_PATTERN = namePatterns.TEAM_PATTERN; $scope.teamFinished = function(team) { $scope.teamCreated({'team': team}); diff --git a/static/js/directives/ui/namespace-input.js b/static/js/directives/ui/namespace-input.js index 422539c12..ea153f3ef 100644 --- a/static/js/directives/ui/namespace-input.js +++ b/static/js/directives/ui/namespace-input.js @@ -15,8 +15,8 @@ angular.module('quay').directive('namespaceInput', function () { 'namespaceTitle': '@namespaceTitle', }, - controller: function($scope, $element) { - $scope.USERNAME_PATTERN = USERNAME_PATTERN; + controller: function($scope, $element, namePatterns) { + $scope.USERNAME_PATTERN = namePatterns.USERNAME_PATTERN; $scope.usernamePattern = new RegExp(USERNAME_PATTERN); $scope.$watch('binding', function(binding) { diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 7f42e3baf..f8a61412a 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -3,6 +3,7 @@ import { quayConfig } from './quay.config.ts'; import quayPages from './quay-pages.module'; import quayRun from './quay.run'; import { angularViewArrayFactory } from './services/angular-view-array/angular-view-array'; +import namePatterns from './constants/name-patterns.constant'; var quayDependencies: string[] = [ @@ -28,19 +29,15 @@ var quayDependencies: string[] = [ if ((window).__config && ((window).__config.MIXPANEL_KEY || (window).__config.MUNCHKIN_KEY || (window).__config.GOOGLE_ANALYTICS_KEY)) { quayDependencies.push('angulartics'); } - if ((window).__config && (window).__config.MIXPANEL_KEY) { quayDependencies.push('angulartics.mixpanel'); } - if ((window).__config && (window).__config.MUNCHKIN_KEY) { quayDependencies.push('angulartics.marketo'); } - if ((window).__config && (window).__config.GOOGLE_ANALYTICS_KEY) { quayDependencies.push('angulartics.google.analytics'); } - if ((window).__config && (window).__config.RECAPTCHA_SITE_KEY) { quayDependencies.push('vcRecaptcha'); } @@ -48,6 +45,7 @@ if ((window).__config && (window).__config.RECAPTCHA_SITE_KEY) { export default angular .module('quay', quayDependencies) .config(quayConfig) + .constant('namePatterns', namePatterns) .factory('AngularViewArray', angularViewArrayFactory) .run(quayRun) .name; \ No newline at end of file diff --git a/static/js/route-builder/route-builder.spec.js b/static/js/route-builder/route-builder.spec.js index fa883cb02..067131a6a 100644 --- a/static/js/route-builder/route-builder.spec.js +++ b/static/js/route-builder/route-builder.spec.js @@ -5,116 +5,120 @@ describe("Service: RouteBuilder", function() { var profiles; var currentProfile; - beforeEach(module('quay')); - - beforeEach(inject(function($injector) { - profiles = [ - {id: 'old-layout', templatePath: '/static/partials/'}, - {id: 'layout', templatePath: '/static/partials/'} - ]; - currentProfile = 'layout'; - routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']); - pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']); - RouteBuilder = $injector.get('RouteBuilder'); - })); - - describe("constructor", function() { - - it("returns a RouteBuilder object", function() { - var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - - expect(routeBuilder).toBeDefined(); - }); - - it("initializes dependencies", function() { - var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - - expect(routeBuilder.routeProvider).toEqual(routeProviderMock); - expect(routeBuilder.pages).toEqual(pagesMock); - expect(routeBuilder.profiles).toBeDefined(); - }); - - it("sets 'profiles' to all given profiles if given current profile does not match any of the given profiles' id", function() { - var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, 'fake-profile'); - - expect(routeBuilder.profiles).toEqual(profiles); - }); - - it("sets 'profiles' to the first given profile with id matching given current profile", function() { - var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - - expect(routeBuilder.profiles).toEqual([profiles[1]]); - }); + it("sanity", function() { + expect(true).toBe(false); }); - describe("otherwise", function() { - var routeBuilder; - - beforeEach(function() { - routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - }); - - it("calls routeProvider to set fallback route with given options", function() { - var options = {1: "option"}; - routeBuilder.otherwise(options); - - expect(routeProviderMock.otherwise.calls.argsFor(0)[0]).toEqual(options); - }); - }); - - describe("route", function() { - var routeBuilder; - var path; - var pagename; - var page; - - beforeEach(function() { - path = '/repository/:namespace/:name'; - pagename = 'repo-view'; - page = { - templateName: 'repository.html', - reloadOnSearch: false, - controller: jasmine.createSpy('pageController'), - flags: {}, - }; - routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - }); - - it("calls pages with given pagename and 'profiles' to get matching page and profile pair", function() { - pagesMock.get.and.returnValue([profiles[1], page]); - routeBuilder.route(path, pagename); - - expect(pagesMock.get.calls.argsFor(0)[0]).toEqual(pagename); - expect(pagesMock.get.calls.argsFor(0)[1]).toEqual(routeBuilder.profiles); - }); - - it("throws error if no matching page/profile pair found", function() { - pagesMock.get.and.returnValue(); - try { - routeBuilder.route(path, pagename); - fail(); - } catch (error) { - expect(error.message).toEqual('Unknown page: ' + pagename); - } - }); - - it("calls routeProvider to set route for given path and options", function() { - pagesMock.get.and.returnValue([profiles[1], page]); - var expectedOptions = { - templateUrl: profiles[1].templatePath + page.templateName, - reloadOnSearch: false, - controller: page.controller, - }; - routeBuilder.route(path, pagename); - - expect(routeProviderMock.when.calls.argsFor(0)[0]).toEqual(path); - expect(routeProviderMock.when.calls.argsFor(0)[1]).toEqual(expectedOptions); - }); - - it("returns itself (the RouteBuilder instance)", function() { - pagesMock.get.and.returnValue([profiles[1], page]); - - expect(routeBuilder.route(path, pagename)).toEqual(routeBuilder); - }); - }); + // beforeEach(module('quay')); + // + // beforeEach(inject(function($injector) { + // profiles = [ + // {id: 'old-layout', templatePath: '/static/partials/'}, + // {id: 'layout', templatePath: '/static/partials/'} + // ]; + // currentProfile = 'layout'; + // routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']); + // pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']); + // RouteBuilder = $injector.get('RouteBuilder'); + // })); + // + // describe("constructor", function() { + // + // it("returns a RouteBuilder object", function() { + // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); + // + // expect(routeBuilder).toBeDefined(); + // }); + // + // it("initializes dependencies", function() { + // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); + // + // expect(routeBuilder.routeProvider).toEqual(routeProviderMock); + // expect(routeBuilder.pages).toEqual(pagesMock); + // expect(routeBuilder.profiles).toBeDefined(); + // }); + // + // it("sets 'profiles' to all given profiles if given current profile does not match any of the given profiles' id", function() { + // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, 'fake-profile'); + // + // expect(routeBuilder.profiles).toEqual(profiles); + // }); + // + // it("sets 'profiles' to the first given profile with id matching given current profile", function() { + // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); + // + // expect(routeBuilder.profiles).toEqual([profiles[1]]); + // }); + // }); + // + // describe("otherwise", function() { + // var routeBuilder; + // + // beforeEach(function() { + // routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); + // }); + // + // it("calls routeProvider to set fallback route with given options", function() { + // var options = {1: "option"}; + // routeBuilder.otherwise(options); + // + // expect(routeProviderMock.otherwise.calls.argsFor(0)[0]).toEqual(options); + // }); + // }); + // + // describe("route", function() { + // var routeBuilder; + // var path; + // var pagename; + // var page; + // + // beforeEach(function() { + // path = '/repository/:namespace/:name'; + // pagename = 'repo-view'; + // page = { + // templateName: 'repository.html', + // reloadOnSearch: false, + // controller: jasmine.createSpy('pageController'), + // flags: {}, + // }; + // routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); + // }); + // + // it("calls pages with given pagename and 'profiles' to get matching page and profile pair", function() { + // pagesMock.get.and.returnValue([profiles[1], page]); + // routeBuilder.route(path, pagename); + // + // expect(pagesMock.get.calls.argsFor(0)[0]).toEqual(pagename); + // expect(pagesMock.get.calls.argsFor(0)[1]).toEqual(routeBuilder.profiles); + // }); + // + // it("throws error if no matching page/profile pair found", function() { + // pagesMock.get.and.returnValue(); + // try { + // routeBuilder.route(path, pagename); + // fail(); + // } catch (error) { + // expect(error.message).toEqual('Unknown page: ' + pagename); + // } + // }); + // + // it("calls routeProvider to set route for given path and options", function() { + // pagesMock.get.and.returnValue([profiles[1], page]); + // var expectedOptions = { + // templateUrl: profiles[1].templatePath + page.templateName, + // reloadOnSearch: false, + // controller: page.controller, + // }; + // routeBuilder.route(path, pagename); + // + // expect(routeProviderMock.when.calls.argsFor(0)[0]).toEqual(path); + // expect(routeProviderMock.when.calls.argsFor(0)[1]).toEqual(expectedOptions); + // }); + // + // it("returns itself (the RouteBuilder instance)", function() { + // pagesMock.get.and.returnValue([profiles[1], page]); + // + // expect(routeBuilder.route(path, pagename)).toEqual(routeBuilder); + // }); + // }); }); \ No newline at end of file diff --git a/static/js/services/angular-view-array/angular-view-array.spec.ts b/static/js/services/angular-view-array/angular-view-array.spec.ts index 2a6e903fc..8a02d35b1 100644 --- a/static/js/services/angular-view-array/angular-view-array.spec.ts +++ b/static/js/services/angular-view-array/angular-view-array.spec.ts @@ -17,136 +17,136 @@ describe("Service: AngularViewArray", () => { expect(angularViewArrayFactory).toBeDefined(); }); - // it("returns a ViewArray object", () => { - // var viewArray: ViewArrayImpl = angularViewArray.create(); - // - // expect(viewArray).toBeDefined(); - // }); + it("returns a ViewArray object", () => { + var viewArray: ViewArrayImpl = angularViewArray.create(); - // describe("returned ViewArray object", () => { - // var viewArray: ViewArrayImpl; - // - // beforeEach(() => { - // viewArray = angularViewArray.create(); - // }); - // - // describe("constructor", () => { - // // TODO - // }); - // - // describe("length", () => { - // - // it("returns the number of entries", () => { - // viewArray.entries = [{}, {}, {}]; - // - // expect(viewArray.length()).toEqual(viewArray.entries.length); - // }); - // }); - // - // describe("get", () => { - // - // it("returns the entry at a given index", () => { - // var index: number = 8; - // viewArray.entries = new Array(10); - // viewArray.entries[index] = 3; - // - // expect(viewArray.get(index)).toEqual(viewArray.entries[index]); - // }); - // }); - // - // describe("push", () => { - // - // it("adds given element to the end of entries", () => { - // var element: number = 3; - // var originalLength: number = viewArray.length(); - // viewArray.push(element); - // - // expect(viewArray.entries.length).toEqual(originalLength + 1); - // expect(viewArray.get(originalLength)).toEqual(element); - // }); - // - // it("sets 'hasEntries' to true", () => { - // viewArray.push(2); - // - // expect(viewArray.hasEntries).toBe(true); - // }); - // - // it("starts timer if 'isVisible' is true", () => { - // spyOn(viewArray, "startTimer_").and.returnValue(null); - // viewArray.isVisible = true; - // viewArray.push(2); - // - // expect(viewArray.startTimer_).toHaveBeenCalled(); - // }); - // - // it("does not start timer if 'isVisible' is false", () => { - // spyOn(viewArray, "startTimer_").and.returnValue(null); - // viewArray.isVisible = false; - // viewArray.push(2); - // - // expect(viewArray.startTimer_).not.toHaveBeenCalled(); - // }); - // }); - // - // describe("toggle", () => { - // - // it("sets 'isVisible' to false if currently true", () => { - // viewArray.isVisible = true; - // viewArray.toggle(); - // - // expect(viewArray.isVisible).toBe(false); - // }); - // - // it("sets 'isVisible' to true if currently false", () => { - // viewArray.isVisible = false; - // viewArray.toggle(); - // - // expect(viewArray.isVisible).toBe(true); - // }); - // }); - // - // describe("setVisible", () => { - // - // it("sets 'isVisible' to false if given false", () => { - // viewArray.setVisible(false); - // - // expect(viewArray.isVisible).toBe(false); - // }); - // - // it("sets 'visibleEntries' to empty array if given false", () => { - // viewArray.setVisible(false); - // - // expect(viewArray.visibleEntries.length).toEqual(0); - // }); - // - // it("shows additional entries if given true", () => { - // spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); - // viewArray.setVisible(true); - // - // expect(viewArray.showAdditionalEntries_).toHaveBeenCalled(); - // }); - // - // it("does not show additional entries if given false", () => { - // spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); - // viewArray.setVisible(false); - // - // expect(viewArray.showAdditionalEntries_).not.toHaveBeenCalled(); - // }); - // - // it("starts timer if given true", () => { - // spyOn(viewArray, "startTimer_").and.returnValue(null); - // viewArray.setVisible(true); - // - // expect(viewArray.startTimer_).toHaveBeenCalled(); - // }); - // - // it("stops timer if given false", () => { - // spyOn(viewArray, "stopTimer_").and.returnValue(null); - // viewArray.setVisible(true); - // - // expect(viewArray.stopTimer_).toHaveBeenCalled(); - // }); - // }); - // }); + expect(viewArray).toBeDefined(); + }); + + describe("returned ViewArray object", () => { + var viewArray: ViewArrayImpl; + + beforeEach(() => { + viewArray = angularViewArray.create(); + }); + + describe("constructor", () => { + // TODO + }); + + describe("length", () => { + + it("returns the number of entries", () => { + viewArray.entries = [{}, {}, {}]; + + expect(viewArray.length()).toEqual(viewArray.entries.length); + }); + }); + + describe("get", () => { + + it("returns the entry at a given index", () => { + var index: number = 8; + viewArray.entries = new Array(10); + viewArray.entries[index] = 3; + + expect(viewArray.get(index)).toEqual(viewArray.entries[index]); + }); + }); + + describe("push", () => { + + it("adds given element to the end of entries", () => { + var element: number = 3; + var originalLength: number = viewArray.length(); + viewArray.push(element); + + expect(viewArray.entries.length).toEqual(originalLength + 1); + expect(viewArray.get(originalLength)).toEqual(element); + }); + + it("sets 'hasEntries' to true", () => { + viewArray.push(2); + + expect(viewArray.hasEntries).toBe(true); + }); + + it("starts timer if 'isVisible' is true", () => { + spyOn(viewArray, "startTimer_").and.returnValue(null); + viewArray.isVisible = true; + viewArray.push(2); + + expect(viewArray.startTimer_).toHaveBeenCalled(); + }); + + it("does not start timer if 'isVisible' is false", () => { + spyOn(viewArray, "startTimer_").and.returnValue(null); + viewArray.isVisible = false; + viewArray.push(2); + + expect(viewArray.startTimer_).not.toHaveBeenCalled(); + }); + }); + + describe("toggle", () => { + + it("sets 'isVisible' to false if currently true", () => { + viewArray.isVisible = true; + viewArray.toggle(); + + expect(viewArray.isVisible).toBe(false); + }); + + it("sets 'isVisible' to true if currently false", () => { + viewArray.isVisible = false; + viewArray.toggle(); + + expect(viewArray.isVisible).toBe(true); + }); + }); + + describe("setVisible", () => { + + it("sets 'isVisible' to false if given false", () => { + viewArray.setVisible(false); + + expect(viewArray.isVisible).toBe(false); + }); + + it("sets 'visibleEntries' to empty array if given false", () => { + viewArray.setVisible(false); + + expect(viewArray.visibleEntries.length).toEqual(0); + }); + + it("shows additional entries if given true", () => { + spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); + viewArray.setVisible(true); + + expect(viewArray.showAdditionalEntries_).toHaveBeenCalled(); + }); + + it("does not show additional entries if given false", () => { + spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); + viewArray.setVisible(false); + + expect(viewArray.showAdditionalEntries_).not.toHaveBeenCalled(); + }); + + it("starts timer if given true", () => { + spyOn(viewArray, "startTimer_").and.returnValue(null); + viewArray.setVisible(true); + + expect(viewArray.startTimer_).toHaveBeenCalled(); + }); + + it("stops timer if given false", () => { + spyOn(viewArray, "stopTimer_").and.returnValue(null); + viewArray.setVisible(true); + + expect(viewArray.stopTimer_).toHaveBeenCalled(); + }); + }); + }); }); }); \ No newline at end of file From bee504d0bae282183f6eafb46b3048f9a139702f Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 19 Jan 2017 02:10:00 -0800 Subject: [PATCH 06/19] switched to running tests using karma-typescript --- karma.conf.js | 12 ++++-------- package.json | 3 ++- static/js/quay.module.ts | 2 +- static/js/route-builder/route-builder.spec.js | 2 +- tsconfig.json | 4 ++-- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index d0c92772a..1f3af9aa6 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -4,7 +4,7 @@ var webpackConfig = require('./webpack.config'); module.exports = function(config) { config.set({ basePath: '', - frameworks: ['jasmine'], + frameworks: ['jasmine', 'karma-typescript'], files: [ // CDN resources 'node_modules/jquery/dist/jquery.js', @@ -28,13 +28,10 @@ module.exports = function(config) { 'static/lib/**/*.js', // Application resources - 'static/js/quay.module.ts', - // 'static/js/**/*.js', + 'static/js/**/*.ts', // Tests 'static/test/**/*.js', - 'static/js/**/*.spec.js', - 'static/js/**/*.spec.ts', ], exclude: [ 'static/js/build/bundle.js', @@ -42,8 +39,7 @@ module.exports = function(config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], - 'static/js/quay.module.ts': ['webpack'], - 'static/js/**/*.spec.ts': ['webpack'], + 'static/js/**/*.ts': ['karma-typescript'], }, webpack: { resolve: webpackConfig.resolve, @@ -66,7 +62,7 @@ module.exports = function(config) { webpackMiddleware: { stats: 'errors-only' }, - reporters: ['dots', 'coverage'], + reporters: ['dots', 'coverage', 'karma-typescript'], coverageReporter: { dir: 'coverage', type: 'html' diff --git a/package.json b/package.json index 42ccf8400..ee8e3841c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "version": "1.0.0", "scripts": { - "test": "./node_modules/.bin/karma start --single-run --browsers Chrome", + "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS", "build": "./node_modules/.bin/webpack --progress -p -v", "watch": "./node_modules/.bin/webpack --watch" }, @@ -47,6 +47,7 @@ "karma-es6-shim": "^1.0.0", "karma-jasmine": "^0.3.8", "karma-phantomjs-launcher": "^1.0.0", + "karma-typescript": "^2.1.6", "karma-webpack": "^1.8.1", "node-sass": "3.10.1", "phantomjs-prebuilt": "^2.1.7", diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index f8a61412a..59ac95ff9 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -1,5 +1,5 @@ import * as angular from 'angular'; -import { quayConfig } from './quay.config.ts'; +import { quayConfig } from './quay.config'; import quayPages from './quay-pages.module'; import quayRun from './quay.run'; import { angularViewArrayFactory } from './services/angular-view-array/angular-view-array'; diff --git a/static/js/route-builder/route-builder.spec.js b/static/js/route-builder/route-builder.spec.js index 067131a6a..b8652316c 100644 --- a/static/js/route-builder/route-builder.spec.js +++ b/static/js/route-builder/route-builder.spec.js @@ -6,7 +6,7 @@ describe("Service: RouteBuilder", function() { var currentProfile; it("sanity", function() { - expect(true).toBe(false); + expect(true).toBe(true); }); // beforeEach(module('quay')); diff --git a/tsconfig.json b/tsconfig.json index d8acb9b2a..2e7ab719c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "baseUrl": ".", "jsx": "react", "module": "commonjs", - "moduleResolution": "node", "outDir": "./build/", "target": "es5", "sourceMap": true, @@ -15,6 +14,7 @@ "node_modules" ], "include": [ - "./static/js/**/*.tsx" + "./static/js/**/*.tsx", + "./static/js/**/*.ts" ] } From 37129687e3b2de1d3cefdce9785a740bb9bc0415 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 19 Jan 2017 03:02:37 -0800 Subject: [PATCH 07/19] created interface for RouteBuilder service --- karma.conf.js | 2 +- static/js/quay.config.ts | 6 +- static/js/quay.module.ts | 2 + static/js/route-builder/route-builder.js | 57 -------- .../route-builder.service.impl.ts | 47 +++++++ .../route-builder.service.spec.ts | 115 ++++++++++++++++ .../js/route-builder/route-builder.service.ts | 6 + static/js/route-builder/route-builder.spec.js | 124 ------------------ 8 files changed, 174 insertions(+), 185 deletions(-) delete mode 100644 static/js/route-builder/route-builder.js create mode 100644 static/js/route-builder/route-builder.service.impl.ts create mode 100644 static/js/route-builder/route-builder.service.spec.ts create mode 100644 static/js/route-builder/route-builder.service.ts delete mode 100644 static/js/route-builder/route-builder.spec.js diff --git a/karma.conf.js b/karma.conf.js index 1f3af9aa6..531630448 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -30,7 +30,7 @@ module.exports = function(config) { // Application resources 'static/js/**/*.ts', - // Tests + // Tests utils 'static/test/**/*.js', ], exclude: [ diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts index cf280ff11..110f926cb 100644 --- a/static/js/quay.config.ts +++ b/static/js/quay.config.ts @@ -1,4 +1,5 @@ import * as Raven from 'raven-js'; +import { RouteBuilder } from './route-builder/route-builder.service'; quayConfig.$inject = [ @@ -57,10 +58,9 @@ export function quayConfig( // index rule to make sure that deep links directly deep into the app continue to work. // WARNING WARNING WARNING - var layoutProfile = 'layout'; - console.log('Using layout profile: ' + layoutProfile); + var layoutProfile: string = 'layout'; - var routeBuilder = new RouteBuilderProvider.$get()($routeProvider, pages, [ + var routeBuilder: RouteBuilder = new RouteBuilderProvider.$get()($routeProvider, pages, [ // Start with the old pages (if we asked for it). {id: 'old-layout', templatePath: '/static/partials/'}, diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 59ac95ff9..3145ffc12 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -3,6 +3,7 @@ import { quayConfig } from './quay.config'; import quayPages from './quay-pages.module'; import quayRun from './quay.run'; import { angularViewArrayFactory } from './services/angular-view-array/angular-view-array'; +import { routeBuilderFactory } from './route-builder/route-builder.service.impl'; import namePatterns from './constants/name-patterns.constant'; @@ -46,6 +47,7 @@ export default angular .module('quay', quayDependencies) .config(quayConfig) .constant('namePatterns', namePatterns) + .factory('RouteBuilder', routeBuilderFactory) .factory('AngularViewArray', angularViewArrayFactory) .run(quayRun) .name; \ No newline at end of file diff --git a/static/js/route-builder/route-builder.js b/static/js/route-builder/route-builder.js deleted file mode 100644 index 8a94706eb..000000000 --- a/static/js/route-builder/route-builder.js +++ /dev/null @@ -1,57 +0,0 @@ -(function() { - 'use strict'; - - angular - .module("quay") - .factory('RouteBuilder', factory); - - factory.$inject = [ - - ]; - - function factory() { - function RouteBuilder(routeProvider, pages, profiles, currentProfile) { - this.routeProvider = routeProvider; - this.pages = pages; - this.profiles = profiles; - - for (var i = 0; i < profiles.length; ++i) { - if (profiles[i].id == currentProfile) { - this.profiles = this.profiles.slice(i); - break; - } - } - } - - RouteBuilder.prototype.otherwise = function(options) { - this.routeProvider.otherwise(options); - }; - - RouteBuilder.prototype.route = function(path, pagename) { - // Lookup the page, matching our lists of profiles. - var pair = this.pages.get(pagename, this.profiles); - if (!pair) { - throw Error('Unknown page: ' + pagename); - } - - // Create the route. - var foundProfile = pair[0]; - var page = pair[1]; - var templateUrl = foundProfile.templatePath + page.templateName; - - var options = jQuery.extend({}, page.flags || {}); - options['templateUrl'] = templateUrl; - options['reloadOnSearch'] = false; - options['controller'] = page.controller; - - this.routeProvider.when(path, options); - - return this; - }; - - return function(routeProvider, pages, profiles, currentProfile) { - return new RouteBuilder(routeProvider, pages, profiles, currentProfile); - } - } - -})(); \ No newline at end of file diff --git a/static/js/route-builder/route-builder.service.impl.ts b/static/js/route-builder/route-builder.service.impl.ts new file mode 100644 index 000000000..f39769965 --- /dev/null +++ b/static/js/route-builder/route-builder.service.impl.ts @@ -0,0 +1,47 @@ +import { RouteBuilder } from './route-builder.service'; + + +export function routeBuilderFactory() { + return (routeProvider, pages, profiles, currentProfile): RouteBuilder => { + return new RouteBuilderImpl(routeProvider, pages, profiles, currentProfile); + } +} + + +export class RouteBuilderImpl implements RouteBuilder { + + constructor(private routeProvider, private pages, public profiles, currentProfile) { + for (let i = 0; i < this.profiles.length; ++i) { + if (this.profiles[i].id == currentProfile) { + this.profiles = this.profiles.slice(i); + break; + } + } + } + + public otherwise(options: any): void { + this.routeProvider.otherwise(options); + } + + public route(path: string, pagename: string): RouteBuilder { + // Lookup the page, matching our lists of profiles. + var pair = this.pages.get(pagename, this.profiles); + if (!pair) { + throw Error('Unknown page: ' + pagename); + } + + // Create the route. + var foundProfile = pair[0]; + var page = pair[1]; + var templateUrl = foundProfile.templatePath + page.templateName; + + var options = jQuery.extend({}, page.flags || {}); + options['templateUrl'] = templateUrl; + options['reloadOnSearch'] = false; + options['controller'] = page.controller; + + this.routeProvider.when(path, options); + + return this; + } +} \ No newline at end of file diff --git a/static/js/route-builder/route-builder.service.spec.ts b/static/js/route-builder/route-builder.service.spec.ts new file mode 100644 index 000000000..22bd51179 --- /dev/null +++ b/static/js/route-builder/route-builder.service.spec.ts @@ -0,0 +1,115 @@ +import { RouteBuilderImpl } from './route-builder.service.impl'; + + +describe("Service: RouteBuilderImpl", () => { + var routeProviderMock; + var pagesMock; + var profiles; + var currentProfile; + + it("sanity", () => { + expect(true).toBe(true); + }); + + beforeEach((() => { + profiles = [ + {id: 'old-layout', templatePath: '/static/partials/'}, + {id: 'layout', templatePath: '/static/partials/'} + ]; + currentProfile = 'layout'; + routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']); + pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']); + })); + + describe("constructor", () => { + + it("returns a RouteBuilder object", () => { + var routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + + expect(routeBuilder).toBeDefined(); + }); + + it("sets 'profiles' to all given profiles if given current profile does not match any of the given profiles' id", () => { + var routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, 'fake-profile'); + + expect(routeBuilder.profiles).toEqual(profiles); + }); + + it("sets 'profiles' to the first given profile with id matching given current profile", () => { + var routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + + expect(routeBuilder.profiles).toEqual([profiles[1]]); + }); + }); + + describe("otherwise", () => { + var routeBuilder; + + beforeEach(() => { + routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + }); + + it("calls routeProvider to set fallback route with given options", () => { + var options = {1: "option"}; + routeBuilder.otherwise(options); + + expect(routeProviderMock.otherwise.calls.argsFor(0)[0]).toEqual(options); + }); + }); + + describe("route", () => { + var routeBuilder; + var path; + var pagename; + var page; + + beforeEach(() => { + path = '/repository/:namespace/:name'; + pagename = 'repo-view'; + page = { + templateName: 'repository.html', + reloadOnSearch: false, + controller: jasmine.createSpy('pageController'), + flags: {}, + }; + routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + }); + + it("calls pages with given pagename and 'profiles' to get matching page and profile pair", () => { + pagesMock.get.and.returnValue([profiles[1], page]); + routeBuilder.route(path, pagename); + + expect(pagesMock.get.calls.argsFor(0)[0]).toEqual(pagename); + expect(pagesMock.get.calls.argsFor(0)[1]).toEqual(routeBuilder.profiles); + }); + + it("throws error if no matching page/profile pair found", () => { + pagesMock.get.and.returnValue(); + try { + routeBuilder.route(path, pagename); + fail(); + } catch (error) { + expect(error.message).toEqual('Unknown page: ' + pagename); + } + }); + + it("calls routeProvider to set route for given path and options", () => { + pagesMock.get.and.returnValue([profiles[1], page]); + var expectedOptions = { + templateUrl: profiles[1].templatePath + page.templateName, + reloadOnSearch: false, + controller: page.controller, + }; + routeBuilder.route(path, pagename); + + expect(routeProviderMock.when.calls.argsFor(0)[0]).toEqual(path); + expect(routeProviderMock.when.calls.argsFor(0)[1]).toEqual(expectedOptions); + }); + + it("returns itself (the RouteBuilder instance)", () => { + pagesMock.get.and.returnValue([profiles[1], page]); + + expect(routeBuilder.route(path, pagename)).toEqual(routeBuilder); + }); + }); +}); \ No newline at end of file diff --git a/static/js/route-builder/route-builder.service.ts b/static/js/route-builder/route-builder.service.ts new file mode 100644 index 000000000..106f03ba1 --- /dev/null +++ b/static/js/route-builder/route-builder.service.ts @@ -0,0 +1,6 @@ +export abstract class RouteBuilder { + + public abstract otherwise(options: any): void; + + public abstract route(path: string, pagename: string): RouteBuilder; +} \ No newline at end of file diff --git a/static/js/route-builder/route-builder.spec.js b/static/js/route-builder/route-builder.spec.js deleted file mode 100644 index b8652316c..000000000 --- a/static/js/route-builder/route-builder.spec.js +++ /dev/null @@ -1,124 +0,0 @@ -describe("Service: RouteBuilder", function() { - var RouteBuilder; - var routeProviderMock; - var pagesMock; - var profiles; - var currentProfile; - - it("sanity", function() { - expect(true).toBe(true); - }); - - // beforeEach(module('quay')); - // - // beforeEach(inject(function($injector) { - // profiles = [ - // {id: 'old-layout', templatePath: '/static/partials/'}, - // {id: 'layout', templatePath: '/static/partials/'} - // ]; - // currentProfile = 'layout'; - // routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']); - // pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']); - // RouteBuilder = $injector.get('RouteBuilder'); - // })); - // - // describe("constructor", function() { - // - // it("returns a RouteBuilder object", function() { - // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - // - // expect(routeBuilder).toBeDefined(); - // }); - // - // it("initializes dependencies", function() { - // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - // - // expect(routeBuilder.routeProvider).toEqual(routeProviderMock); - // expect(routeBuilder.pages).toEqual(pagesMock); - // expect(routeBuilder.profiles).toBeDefined(); - // }); - // - // it("sets 'profiles' to all given profiles if given current profile does not match any of the given profiles' id", function() { - // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, 'fake-profile'); - // - // expect(routeBuilder.profiles).toEqual(profiles); - // }); - // - // it("sets 'profiles' to the first given profile with id matching given current profile", function() { - // var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - // - // expect(routeBuilder.profiles).toEqual([profiles[1]]); - // }); - // }); - // - // describe("otherwise", function() { - // var routeBuilder; - // - // beforeEach(function() { - // routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - // }); - // - // it("calls routeProvider to set fallback route with given options", function() { - // var options = {1: "option"}; - // routeBuilder.otherwise(options); - // - // expect(routeProviderMock.otherwise.calls.argsFor(0)[0]).toEqual(options); - // }); - // }); - // - // describe("route", function() { - // var routeBuilder; - // var path; - // var pagename; - // var page; - // - // beforeEach(function() { - // path = '/repository/:namespace/:name'; - // pagename = 'repo-view'; - // page = { - // templateName: 'repository.html', - // reloadOnSearch: false, - // controller: jasmine.createSpy('pageController'), - // flags: {}, - // }; - // routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile); - // }); - // - // it("calls pages with given pagename and 'profiles' to get matching page and profile pair", function() { - // pagesMock.get.and.returnValue([profiles[1], page]); - // routeBuilder.route(path, pagename); - // - // expect(pagesMock.get.calls.argsFor(0)[0]).toEqual(pagename); - // expect(pagesMock.get.calls.argsFor(0)[1]).toEqual(routeBuilder.profiles); - // }); - // - // it("throws error if no matching page/profile pair found", function() { - // pagesMock.get.and.returnValue(); - // try { - // routeBuilder.route(path, pagename); - // fail(); - // } catch (error) { - // expect(error.message).toEqual('Unknown page: ' + pagename); - // } - // }); - // - // it("calls routeProvider to set route for given path and options", function() { - // pagesMock.get.and.returnValue([profiles[1], page]); - // var expectedOptions = { - // templateUrl: profiles[1].templatePath + page.templateName, - // reloadOnSearch: false, - // controller: page.controller, - // }; - // routeBuilder.route(path, pagename); - // - // expect(routeProviderMock.when.calls.argsFor(0)[0]).toEqual(path); - // expect(routeProviderMock.when.calls.argsFor(0)[1]).toEqual(expectedOptions); - // }); - // - // it("returns itself (the RouteBuilder instance)", function() { - // pagesMock.get.and.returnValue([profiles[1], page]); - // - // expect(routeBuilder.route(path, pagename)).toEqual(routeBuilder); - // }); - // }); -}); \ No newline at end of file From 50583184541045208026131bf6976dfc7b8f1b22 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 19 Jan 2017 12:35:59 -0800 Subject: [PATCH 08/19] use minified angularjs --- external_libraries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external_libraries.py b/external_libraries.py index 28d697376..d99431283 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -7,7 +7,7 @@ LOCAL_DIRECTORY = '/static/ldn/' EXTERNAL_JS = [ 'code.jquery.com/jquery.js', 'netdna.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js', - 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js', + 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-route.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-sanitize.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-animate.min.js', From 8f0f16a55137ea0706387f6ea364e5edadce910a Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 19 Jan 2017 14:54:00 -0800 Subject: [PATCH 09/19] single entrypoint for Webpack --- karma.conf.js | 4 +- static/js/app.tsx | 5 --- static/js/constants/pages.constant.ts | 28 +++++++++++++ .../components/pages/repo-page/main.tsx | 39 ++++++++++++------- static/js/directives/ui/namespace-input.js | 2 +- static/js/quay-pages.module.ts | 34 +++------------- webpack.config.js | 2 +- 7 files changed, 62 insertions(+), 52 deletions(-) delete mode 100644 static/js/app.tsx create mode 100644 static/js/constants/pages.constant.ts diff --git a/karma.conf.js b/karma.conf.js index 531630448..d8bf7bcfa 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -28,7 +28,7 @@ module.exports = function(config) { 'static/lib/**/*.js', // Application resources - 'static/js/**/*.ts', + 'static/js/**/*.ts*', // Tests utils 'static/test/**/*.js', @@ -39,7 +39,7 @@ module.exports = function(config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], - 'static/js/**/*.ts': ['karma-typescript'], + 'static/js/**/*.ts*': ['karma-typescript'], }, webpack: { resolve: webpackConfig.resolve, diff --git a/static/js/app.tsx b/static/js/app.tsx deleted file mode 100644 index ad9cebbc9..000000000 --- a/static/js/app.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// Import Components -import {rpDirectives as repoPage} from "./directives/components/pages/repo-page/main"; - -// Init for each page -repoPage(); diff --git a/static/js/constants/pages.constant.ts b/static/js/constants/pages.constant.ts new file mode 100644 index 000000000..1c77e790c --- /dev/null +++ b/static/js/constants/pages.constant.ts @@ -0,0 +1,28 @@ +export default { + '_pages': {}, + + 'create': function (pageName, templateName, opt_controller, opt_flags, opt_profiles) { + var profiles = opt_profiles || ['old-layout', 'layout']; + for (var i = 0; i < profiles.length; ++i) { + this._pages[profiles[i] + ':' + pageName] = { + 'name': pageName, + 'controller': opt_controller, + 'templateName': templateName, + 'flags': opt_flags || {} + }; + } + }, + + 'get': function (pageName, profiles) { + for (var i = 0; i < profiles.length; ++i) { + var current = profiles[i]; + var key = current.id + ':' + pageName; + var page = this._pages[key]; + if (page) { + return [current, page]; + } + } + + return null; + } +}; \ No newline at end of file diff --git a/static/js/directives/components/pages/repo-page/main.tsx b/static/js/directives/components/pages/repo-page/main.tsx index 7fb8e0453..094381f92 100644 --- a/static/js/directives/components/pages/repo-page/main.tsx +++ b/static/js/directives/components/pages/repo-page/main.tsx @@ -1,22 +1,31 @@ -import "sass/repo-page/repo-page.scss"; -import * as angular from "angular"; -import quayPages from '../../../../quay-pages.module'; - +import "../../../../../css/directives/components/pages/repo-page/repo-page.scss"; import repoHeader from "./header"; import repoSidebar from "./sidebar"; import repoBody from "./body"; -export function rpDirectives(){ - angular.module(quayPages).directive('rpHeader', function(reactDirective) { - return reactDirective(repoHeader); - }); +rpHeaderDirective.$inject = [ + 'reactDirective', +]; - angular.module(quayPages).directive('rpSidebar', function(reactDirective) { - return reactDirective(repoSidebar); - }); - - angular.module(quayPages).directive('rpBody', function(reactDirective, ApiService) { - return reactDirective(repoBody, undefined, {}, {api: ApiService}); - }); +export function rpHeaderDirective(reactDirective) { + return reactDirective(repoHeader); } + +rpSidebarDirective.$inject = [ + 'reactDirective', +]; + +export function rpSidebarDirective(reactDirective) { + return reactDirective(repoSidebar); +} + + +rpBodyDirective.$inject = [ + 'reactDirective', + 'ApiService', +]; + +export function rpBodyDirective(reactDirective, ApiService) { + return reactDirective(repoBody, undefined, {}, {api: ApiService}); +} \ No newline at end of file diff --git a/static/js/directives/ui/namespace-input.js b/static/js/directives/ui/namespace-input.js index 9837d7290..11f2470cc 100644 --- a/static/js/directives/ui/namespace-input.js +++ b/static/js/directives/ui/namespace-input.js @@ -17,7 +17,7 @@ angular.module('quay').directive('namespaceInput', function () { }, controller: function($scope, $element, namePatterns) { $scope.USERNAME_PATTERN = namePatterns.USERNAME_PATTERN; - $scope.usernamePattern = new RegExp(USERNAME_PATTERN); + $scope.usernamePattern = new RegExp(namePatterns.USERNAME_PATTERN); $scope.$watch('binding', function(binding) { if (!binding) { diff --git a/static/js/quay-pages.module.ts b/static/js/quay-pages.module.ts index 7a203e984..072307c58 100644 --- a/static/js/quay-pages.module.ts +++ b/static/js/quay-pages.module.ts @@ -1,34 +1,12 @@ import * as angular from 'angular'; +import { rpHeaderDirective, rpBodyDirective, rpSidebarDirective } from './directives/components/pages/repo-page/main'; +import pages from './constants/pages.constant'; export default angular .module('quayPages', []) - .constant('pages', { - '_pages': {}, - - 'create': function(pageName, templateName, opt_controller, opt_flags, opt_profiles) { - var profiles = opt_profiles || ['old-layout', 'layout']; - for (var i = 0; i < profiles.length; ++i) { - this._pages[profiles[i] + ':' + pageName] = { - 'name': pageName, - 'controller': opt_controller, - 'templateName': templateName, - 'flags': opt_flags || {} - }; - } - }, - - 'get': function(pageName, profiles) { - for (var i = 0; i < profiles.length; ++i) { - var current = profiles[i]; - var key = current.id + ':' + pageName; - var page = this._pages[key]; - if (page) { - return [current, page]; - } - } - - return null; - } - }) + .constant('pages', pages) + .directive('rpHeader', rpHeaderDirective) + .directive('rpSidebar', rpSidebarDirective) + .directive('rpBody', rpBodyDirective) .name; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 9f542185e..7d21632db 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/app.tsx", "./static/js/quay.module.ts"], + entry: "./static/js/quay.module.ts", output: { path: path.resolve(__dirname, "static/js/build"), filename: "bundle.js" From 8dc9cf21d7f5028d7103524d31bfcb2b16c2aa92 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 19 Jan 2017 20:02:57 -0800 Subject: [PATCH 10/19] fixed karma-webpack, also added jasmine-ts to run tests in pure NodeJS --- karma.conf.js | 7 ++++--- package.json | 3 ++- static/js/route-builder/route-builder.service.impl.ts | 2 +- static/js/route-builder/route-builder.service.spec.ts | 4 ---- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index d8bf7bcfa..c728af7e0 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -4,7 +4,7 @@ var webpackConfig = require('./webpack.config'); module.exports = function(config) { config.set({ basePath: '', - frameworks: ['jasmine', 'karma-typescript'], + frameworks: ['jasmine'], files: [ // CDN resources 'node_modules/jquery/dist/jquery.js', @@ -28,7 +28,7 @@ module.exports = function(config) { 'static/lib/**/*.js', // Application resources - 'static/js/**/*.ts*', + 'static/js/**/*.spec.ts*', // Tests utils 'static/test/**/*.js', @@ -39,7 +39,8 @@ module.exports = function(config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], - 'static/js/**/*.ts*': ['karma-typescript'], + // 'static/js/**/*.ts*': ['karma-typescript'], + 'static/js/**/*.ts*': ['webpack'], }, webpack: { resolve: webpackConfig.resolve, diff --git a/package.json b/package.json index ee8e3841c..2e92b8daa 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "version": "1.0.0", "scripts": { "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS", + "test:node": "./node_modules/.bin/jasmine-ts './static/js/**/*.spec.ts'", "build": "./node_modules/.bin/webpack --progress -p -v", "watch": "./node_modules/.bin/webpack --watch" }, @@ -41,13 +42,13 @@ "angular-mocks": "^1.5.3", "css-loader": "0.25.0", "jasmine-core": "^2.5.2", + "jasmine-ts": "0.0.3", "karma": "^0.13.22", "karma-chrome-launcher": "^2.0.0", "karma-coverage": "^0.5.5", "karma-es6-shim": "^1.0.0", "karma-jasmine": "^0.3.8", "karma-phantomjs-launcher": "^1.0.0", - "karma-typescript": "^2.1.6", "karma-webpack": "^1.8.1", "node-sass": "3.10.1", "phantomjs-prebuilt": "^2.1.7", diff --git a/static/js/route-builder/route-builder.service.impl.ts b/static/js/route-builder/route-builder.service.impl.ts index f39769965..8f6382abb 100644 --- a/static/js/route-builder/route-builder.service.impl.ts +++ b/static/js/route-builder/route-builder.service.impl.ts @@ -35,7 +35,7 @@ export class RouteBuilderImpl implements RouteBuilder { var page = pair[1]; var templateUrl = foundProfile.templatePath + page.templateName; - var options = jQuery.extend({}, page.flags || {}); + var options = page.flags || {}; options['templateUrl'] = templateUrl; options['reloadOnSearch'] = false; options['controller'] = page.controller; diff --git a/static/js/route-builder/route-builder.service.spec.ts b/static/js/route-builder/route-builder.service.spec.ts index 22bd51179..ef29b5ba4 100644 --- a/static/js/route-builder/route-builder.service.spec.ts +++ b/static/js/route-builder/route-builder.service.spec.ts @@ -7,10 +7,6 @@ describe("Service: RouteBuilderImpl", () => { var profiles; var currentProfile; - it("sanity", () => { - expect(true).toBe(true); - }); - beforeEach((() => { profiles = [ {id: 'old-layout', templatePath: '/static/partials/'}, From 615e23367196ded3a653e54116cc1e9a9fb9a2d5 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 20 Jan 2017 16:24:55 -0800 Subject: [PATCH 11/19] moved Angular routes to separate module; load Webpack bundle before other main scripts --- endpoints/common.py | 3 + karma.conf.js | 20 +-- package.json | 3 +- static/js/constants/name-patterns.constant.ts | 3 + static/js/constants/pages.constant.ts | 17 +++ .../js/directives/ui/create-robot-dialog.js | 4 +- static/js/directives/ui/create-team-dialog.js | 4 +- static/js/directives/ui/namespace-input.js | 6 +- static/js/quay.config.ts | 137 +---------------- static/js/quay.module.ts | 6 +- static/js/quay.routes.ts | 141 ++++++++++++++++++ static/test/jasmine.json | 6 + templates/base.html | 2 - webpack.config.js | 1 + 14 files changed, 186 insertions(+), 167 deletions(-) create mode 100644 static/js/quay.routes.ts create mode 100644 static/test/jasmine.json diff --git a/endpoints/common.py b/endpoints/common.py index 6fd7ff348..322bc2427 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -166,6 +166,9 @@ def render_page_template(name, route_data=None, **kwargs): file_lists = [library_styles, main_styles, library_scripts, main_scripts] for file_list in file_lists: file_list.sort() + # Ensure Webpack bundle is first script on page + if 'js/build/bundle.js' in main_scripts: main_scripts.remove('js/build/bundle.js') + main_scripts = ['js/build/bundle.js'] + main_scripts else: library_styles = [] main_styles = ['dist/quay-frontend.css'] diff --git a/karma.conf.js b/karma.conf.js index c728af7e0..7ace21f46 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -39,27 +39,9 @@ module.exports = function(config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], - // 'static/js/**/*.ts*': ['karma-typescript'], 'static/js/**/*.ts*': ['webpack'], }, - webpack: { - resolve: webpackConfig.resolve, - externals: webpackConfig.externals, - module: { - loaders: [ - { - test: /\.tsx?$/, - loader: "ts-loader", - exclude: /node_modules/ - }, - { - test: /\.scss$/, - loaders: ['style', 'css', 'sass'], - exclude: /node_modules/ - }, - ] - } - }, + webpack: webpackConfig, webpackMiddleware: { stats: 'errors-only' }, diff --git a/package.json b/package.json index 2e92b8daa..143e082a5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "1.0.0", "scripts": { "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS", - "test:node": "./node_modules/.bin/jasmine-ts './static/js/**/*.spec.ts'", + "test:node": "JASMINE_CONFIG_PATH=static/test/jasmine.json ./node_modules/.bin/jasmine-ts './static/js/**/*.spec.ts'", "build": "./node_modules/.bin/webpack --progress -p -v", "watch": "./node_modules/.bin/webpack --watch" }, @@ -36,6 +36,7 @@ "devDependencies": { "@types/angular": "1.5.16", "@types/angular-mocks": "^1.5.8", + "@types/angular-route": "^1.3.3", "@types/jasmine": "^2.5.41", "@types/react": "0.14.39", "@types/react-dom": "0.14.17", diff --git a/static/js/constants/name-patterns.constant.ts b/static/js/constants/name-patterns.constant.ts index 6f4f1a586..a90b027ff 100644 --- a/static/js/constants/name-patterns.constant.ts +++ b/static/js/constants/name-patterns.constant.ts @@ -1,3 +1,6 @@ +/** + * Regex patterns to for validating account names. + */ export default { TEAM_PATTERN: '^[a-z][a-z0-9]+$', ROBOT_PATTERN: '^[a-z][a-z0-9_]{1,254}$', diff --git a/static/js/constants/pages.constant.ts b/static/js/constants/pages.constant.ts index 1c77e790c..57b6eb7fd 100644 --- a/static/js/constants/pages.constant.ts +++ b/static/js/constants/pages.constant.ts @@ -1,6 +1,18 @@ +/** + * Manages the creation and retrieval of pages (route + controller) + * TODO: Convert to class/Angular service + */ export default { '_pages': {}, + /** + * Create a page. + * @param pageName The name of the page. + * @param templateName The file name of the template. + * @param opt_controller Controller for the page. + * @param opt_flags Additional flags passed to route provider. + * @param opt_profiles Available profiles. + */ 'create': function (pageName, templateName, opt_controller, opt_flags, opt_profiles) { var profiles = opt_profiles || ['old-layout', 'layout']; for (var i = 0; i < profiles.length; ++i) { @@ -13,6 +25,11 @@ export default { } }, + /** + * Retrieve a registered page. + * @param pageName The name of the page. + * @param profiles Available profiles to search. + */ 'get': function (pageName, profiles) { for (var i = 0; i < profiles.length; ++i) { var current = profiles[i]; diff --git a/static/js/directives/ui/create-robot-dialog.js b/static/js/directives/ui/create-robot-dialog.js index 53e18023a..6b2a24b23 100644 --- a/static/js/directives/ui/create-robot-dialog.js +++ b/static/js/directives/ui/create-robot-dialog.js @@ -12,8 +12,8 @@ angular.module('quay').directive('createRobotDialog', function () { 'info': '=info', 'robotCreated': '&robotCreated' }, - controller: function($scope, $element, ApiService, UserService, namePatterns) { - $scope.ROBOT_PATTERN = namePatterns.ROBOT_PATTERN; + controller: function($scope, $element, ApiService, UserService, NAME_PATTERNS) { + $scope.ROBOT_PATTERN = NAME_PATTERNS.ROBOT_PATTERN; $scope.robotFinished = function(robot) { $scope.robotCreated({'robot': robot}); diff --git a/static/js/directives/ui/create-team-dialog.js b/static/js/directives/ui/create-team-dialog.js index 49e71595b..d66f1bc44 100644 --- a/static/js/directives/ui/create-team-dialog.js +++ b/static/js/directives/ui/create-team-dialog.js @@ -12,8 +12,8 @@ angular.module('quay').directive('createTeamDialog', function () { 'info': '=info', 'teamCreated': '&teamCreated' }, - controller: function($scope, $element, ApiService, UserService, namePatterns) { - $scope.TEAM_PATTERN = namePatterns.TEAM_PATTERN; + controller: function($scope, $element, ApiService, UserService, NAME_PATTERNS) { + $scope.TEAM_PATTERN = NAME_PATTERNS.TEAM_PATTERN; $scope.teamFinished = function(team) { $scope.teamCreated({'team': team}); diff --git a/static/js/directives/ui/namespace-input.js b/static/js/directives/ui/namespace-input.js index 11f2470cc..222834a47 100644 --- a/static/js/directives/ui/namespace-input.js +++ b/static/js/directives/ui/namespace-input.js @@ -15,9 +15,9 @@ angular.module('quay').directive('namespaceInput', function () { 'namespaceTitle': '@namespaceTitle', }, - controller: function($scope, $element, namePatterns) { - $scope.USERNAME_PATTERN = namePatterns.USERNAME_PATTERN; - $scope.usernamePattern = new RegExp(namePatterns.USERNAME_PATTERN); + controller: function($scope, $element, NAME_PATTERNS) { + $scope.USERNAME_PATTERN = NAME_PATTERNS.USERNAME_PATTERN; + $scope.usernamePattern = new RegExp(NAME_PATTERNS.USERNAME_PATTERN); $scope.$watch('binding', function(binding) { if (!binding) { diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts index 110f926cb..3a9e90551 100644 --- a/static/js/quay.config.ts +++ b/static/js/quay.config.ts @@ -1,5 +1,4 @@ import * as Raven from 'raven-js'; -import { RouteBuilder } from './route-builder/route-builder.service'; quayConfig.$inject = [ @@ -7,10 +6,6 @@ quayConfig.$inject = [ 'cfpLoadingBarProvider', '$tooltipProvider', '$compileProvider', - '$routeProvider', - '$locationProvider', - 'pages', - 'RouteBuilderProvider', 'RestangularProvider', '$analyticsProvider', ]; @@ -20,17 +15,13 @@ export function quayConfig( cfpLoadingBarProvider, $tooltipProvider, $compileProvider, - $routeProvider, - $locationProvider, - pages, - RouteBuilderProvider, RestangularProvider, $analyticsProvider) { cfpLoadingBarProvider.includeSpinner = false; // decorate the tooltip getter var tooltipFactory = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; - $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window) { + $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { if ('ontouchstart' in $window) { var existing = tooltipFactory.apply(this, arguments); return function(element) { @@ -51,132 +42,6 @@ export function quayConfig( $compileProvider.debugInfoEnabled(false); } - $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 layoutProfile: string = 'layout'; - - var routeBuilder: RouteBuilder = new RouteBuilderProvider.$get()($routeProvider, pages, [ - // Start with the old pages (if we asked for it). - {id: 'old-layout', templatePath: '/static/partials/'}, - - // Fallback back combined new/existing pages. - {id: 'layout', templatePath: '/static/partials/'} - ], layoutProfile); - - if ((window).__features.SUPER_USERS) { - // QE Management - routeBuilder.route('/superuser/', 'superuser') - - // QE Setup - .route('/setup/', 'setup'); - } - - routeBuilder - // 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') - - // 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') - - // Enterprise marketing page - .route('/enterprise', 'enterprise') - - // 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'); - // 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):/); diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 3145ffc12..257c70f91 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -4,7 +4,8 @@ import quayPages from './quay-pages.module'; import quayRun from './quay.run'; import { angularViewArrayFactory } from './services/angular-view-array/angular-view-array'; import { routeBuilderFactory } from './route-builder/route-builder.service.impl'; -import namePatterns from './constants/name-patterns.constant'; +import NAME_PATTERNS from './constants/name-patterns.constant'; +import { routeConfig } from './quay.routes'; var quayDependencies: string[] = [ @@ -46,7 +47,8 @@ if ((window).__config && (window).__config.RECAPTCHA_SITE_KEY) { export default angular .module('quay', quayDependencies) .config(quayConfig) - .constant('namePatterns', namePatterns) + .config(routeConfig) + .constant('NAME_PATTERNS', NAME_PATTERNS) .factory('RouteBuilder', routeBuilderFactory) .factory('AngularViewArray', angularViewArrayFactory) .run(quayRun) diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts new file mode 100644 index 000000000..e74a735b7 --- /dev/null +++ b/static/js/quay.routes.ts @@ -0,0 +1,141 @@ +import { RouteBuilder } from './route-builder/route-builder.service'; +import pages from './constants/pages.constant'; + + +routeConfig.$inject = [ + 'pages', + 'RouteBuilderProvider', + '$routeProvider', + '$locationProvider', +]; + +export function routeConfig( + pages: any, + RouteBuilderProvider: any, + $routeProvider: ng.route.IRouteProvider, + $locationProvider: ng.ILocationProvider) { + $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 layoutProfile: string = 'layout'; + + var routeBuilder: RouteBuilder = new RouteBuilderProvider.$get()($routeProvider, pages, [ + // Start with the old pages (if we asked for it). + {id: 'old-layout', templatePath: '/static/partials/'}, + + // Fallback back combined new/existing pages. + {id: 'layout', templatePath: '/static/partials/'} + ], layoutProfile); + + if ((window).__features.SUPER_USERS) { + // QE Management + routeBuilder.route('/superuser/', 'superuser') + // QE Setup + .route('/setup/', 'setup'); + } + + routeBuilder + // 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') + + // 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') + + // Enterprise marketing page + .route('/enterprise', 'enterprise') + + // 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'); +} \ No newline at end of file diff --git a/static/test/jasmine.json b/static/test/jasmine.json new file mode 100644 index 000000000..90bfcf025 --- /dev/null +++ b/static/test/jasmine.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "./static/js", + "spec_files": [ + "**/*.spec.js" + ] +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index e455ac609..da1baddf3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -64,8 +64,6 @@ {% endblock %} - - {% for script_path, cache_buster in main_scripts %} {% endfor %} diff --git a/webpack.config.js b/webpack.config.js index 7d21632db..be272f0a3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,7 @@ var config = { "sass": path.resolve('./static/css/directives/components/pages/') } }, + // Use window.angular to maintain compatibility with non-Webpack components externals: { "angular": "angular", }, From eea2a18c3f5466fdf0169edcfaa4ec8212ed9fbb Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 21 Jan 2017 00:14:37 -0800 Subject: [PATCH 12/19] move window.__config to an Angular constant; refactor AngularViewArray -> ViewArray + ViewArrayFactory --- static/js/constants/quay-config.constant.ts | 10 ++ static/js/directives/ui/build-logs-view.js | 4 +- static/js/directives/ui/image-feature-view.js | 2 +- .../directives/ui/image-vulnerability-view.js | 2 +- static/js/quay.config.ts | 8 +- static/js/quay.module.ts | 16 +- static/js/quay.run.ts | 6 +- .../angular-view-array.spec.ts | 152 ------------------ .../angular-view-array/angular-view-array.ts | 24 --- .../services/angular-view-array/view-array.ts | 12 -- static/js/services/table-service.js | 4 +- .../view-array/view-array.factory.spec.ts | 27 ++++ .../services/view-array/view-array.factory.ts | 29 ++++ .../view-array/view-array.impl.spec.ts | 140 ++++++++++++++++ .../view-array.impl.ts | 49 +++--- static/js/services/view-array/view-array.ts | 62 +++++++ 16 files changed, 316 insertions(+), 231 deletions(-) create mode 100644 static/js/constants/quay-config.constant.ts delete mode 100644 static/js/services/angular-view-array/angular-view-array.spec.ts delete mode 100644 static/js/services/angular-view-array/angular-view-array.ts delete mode 100644 static/js/services/angular-view-array/view-array.ts create mode 100644 static/js/services/view-array/view-array.factory.spec.ts create mode 100644 static/js/services/view-array/view-array.factory.ts create mode 100644 static/js/services/view-array/view-array.impl.spec.ts rename static/js/services/{angular-view-array => view-array}/view-array.impl.ts (55%) create mode 100644 static/js/services/view-array/view-array.ts diff --git a/static/js/constants/quay-config.constant.ts b/static/js/constants/quay-config.constant.ts new file mode 100644 index 000000000..400207b75 --- /dev/null +++ b/static/js/constants/quay-config.constant.ts @@ -0,0 +1,10 @@ +/** + * Configuration data set. + */ +export const CONFIG: any = (window).__config; + + +/** + * REST API route information. + */ +export const ENDPOINTS: any = (window).__endpoints; \ No newline at end of file diff --git a/static/js/directives/ui/build-logs-view.js b/static/js/directives/ui/build-logs-view.js index 9d1dd7569..e26d10b2b 100644 --- a/static/js/directives/ui/build-logs-view.js +++ b/static/js/directives/ui/build-logs-view.js @@ -13,7 +13,7 @@ angular.module('quay').directive('buildLogsView', function () { 'useTimestamps': '=useTimestamps', 'buildUpdated': '&buildUpdated' }, - controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray, + controller: function($scope, $element, $interval, $sanitize, ansi2html, ViewArrayFactory, AngularPollChannel, ApiService, Restangular, UtilService) { var result = $element.find('#copyButton').clipboardCopy(); @@ -53,7 +53,7 @@ angular.module('quay').directive('buildLogsView', function () { var entry = logs[i]; var type = entry['type'] || 'entry'; if (type == 'command' || type == 'phase' || type == 'error') { - entry['logs'] = AngularViewArray.create(); + entry['logs'] = ViewArrayFactory.create(); entry['index'] = $scope.logStartIndex + i; $scope.logEntries.push(entry); diff --git a/static/js/directives/ui/image-feature-view.js b/static/js/directives/ui/image-feature-view.js index 175089318..484bfe8f9 100644 --- a/static/js/directives/ui/image-feature-view.js +++ b/static/js/directives/ui/image-feature-view.js @@ -13,7 +13,7 @@ angular.module('quay').directive('imageFeatureView', function () { 'image': '=image', 'isEnabled': '=isEnabled' }, - controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService, TableService) { + controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArrayFactory, ImageMetadataService, TableService) { $scope.options = { 'filter': null, 'predicate': 'fixableScore', diff --git a/static/js/directives/ui/image-vulnerability-view.js b/static/js/directives/ui/image-vulnerability-view.js index afbf9671e..25d49bb63 100644 --- a/static/js/directives/ui/image-vulnerability-view.js +++ b/static/js/directives/ui/image-vulnerability-view.js @@ -13,7 +13,7 @@ angular.module('quay').directive('imageVulnerabilityView', function () { 'image': '=image', 'isEnabled': '=isEnabled' }, - controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService, TableService) { + controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArrayFactory, ImageMetadataService, TableService) { $scope.options = { 'filter': null, 'fixableVulns': false, diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts index 3a9e90551..f7ba10686 100644 --- a/static/js/quay.config.ts +++ b/static/js/quay.config.ts @@ -3,6 +3,7 @@ import * as Raven from 'raven-js'; quayConfig.$inject = [ '$provide', + 'CONFIG', 'cfpLoadingBarProvider', '$tooltipProvider', '$compileProvider', @@ -12,6 +13,7 @@ quayConfig.$inject = [ export function quayConfig( $provide, + CONFIG, cfpLoadingBarProvider, $tooltipProvider, $compileProvider, @@ -38,7 +40,7 @@ export function quayConfig( return tooltipFactory.apply(this, arguments); }; - if (!(window).__config['DEBUG']) { + if (!CONFIG['DEBUG']) { $compileProvider.debugInfoEnabled(false); } @@ -50,12 +52,12 @@ export function quayConfig( RestangularProvider.setBaseUrl('/api/v1/'); // Configure analytics. - if ((window).__config && (window).__config.MIXPANEL_KEY) { + if (CONFIG && CONFIG.MIXPANEL_KEY) { $analyticsProvider.virtualPageviews(true); } // Configure sentry. - if ((window).__config && (window).__config.SENTRY_PUBLIC_DSN) { + if (CONFIG && CONFIG.SENTRY_PUBLIC_DSN) { $provide.decorator("$exceptionHandler", function($delegate) { return function(ex, cause) { $delegate(ex, cause); diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 257c70f91..b61c0a4d4 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -2,10 +2,11 @@ import * as angular from 'angular'; import { quayConfig } from './quay.config'; import quayPages from './quay-pages.module'; import quayRun from './quay.run'; -import { angularViewArrayFactory } from './services/angular-view-array/angular-view-array'; +import { ViewArrayFactory } from './services/view-array/view-array.factory'; import { routeBuilderFactory } from './route-builder/route-builder.service.impl'; import NAME_PATTERNS from './constants/name-patterns.constant'; import { routeConfig } from './quay.routes'; +import { CONFIG } from './constants/quay-config.constant'; var quayDependencies: string[] = [ @@ -28,19 +29,19 @@ var quayDependencies: string[] = [ 'react' ]; -if ((window).__config && ((window).__config.MIXPANEL_KEY || (window).__config.MUNCHKIN_KEY || (window).__config.GOOGLE_ANALYTICS_KEY)) { +if (CONFIG && (CONFIG.MIXPANEL_KEY || CONFIG.MUNCHKIN_KEY || CONFIG.GOOGLE_ANALYTICS_KEY)) { quayDependencies.push('angulartics'); } -if ((window).__config && (window).__config.MIXPANEL_KEY) { +if (CONFIG && CONFIG.MIXPANEL_KEY) { quayDependencies.push('angulartics.mixpanel'); } -if ((window).__config && (window).__config.MUNCHKIN_KEY) { +if (CONFIG && CONFIG.MUNCHKIN_KEY) { quayDependencies.push('angulartics.marketo'); } -if ((window).__config && (window).__config.GOOGLE_ANALYTICS_KEY) { +if (CONFIG && CONFIG.GOOGLE_ANALYTICS_KEY) { quayDependencies.push('angulartics.google.analytics'); } -if ((window).__config && (window).__config.RECAPTCHA_SITE_KEY) { +if (CONFIG && CONFIG.RECAPTCHA_SITE_KEY) { quayDependencies.push('vcRecaptcha'); } @@ -49,7 +50,8 @@ export default angular .config(quayConfig) .config(routeConfig) .constant('NAME_PATTERNS', NAME_PATTERNS) + .constant('CONFIG', CONFIG) .factory('RouteBuilder', routeBuilderFactory) - .factory('AngularViewArray', angularViewArrayFactory) + .service('ViewArrayFactory', ViewArrayFactory) .run(quayRun) .name; \ No newline at end of file diff --git a/static/js/quay.run.ts b/static/js/quay.run.ts index a8a2a139f..e7513c560 100644 --- a/static/js/quay.run.ts +++ b/static/js/quay.run.ts @@ -14,6 +14,7 @@ quayRun.$inject = [ '$anchorScroll', 'UtilService', 'MetaService', + 'CONFIG', ]; export default function quayRun( @@ -28,8 +29,9 @@ export default function quayRun( Features, $anchorScroll, UtilService, - MetaService) { - var defaultTitle = (window).__config['REGISTRY_TITLE'] || 'Quay Container Registry'; + MetaService, + CONFIG) { + var defaultTitle = CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; // Handle session security. Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], {'_csrf_token': (window).__token || ''}); diff --git a/static/js/services/angular-view-array/angular-view-array.spec.ts b/static/js/services/angular-view-array/angular-view-array.spec.ts deleted file mode 100644 index 8a02d35b1..000000000 --- a/static/js/services/angular-view-array/angular-view-array.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { angularViewArrayFactory } from './angular-view-array'; -import { ViewArrayImpl } from './view-array.impl'; - - -describe("Service: AngularViewArray", () => { - var angularViewArray: any; - var $interval: ng.IIntervalService; - - beforeEach(inject(($injector: ng.auto.IInjectorService) => { - $interval = $injector.get('$interval'); - angularViewArray = angularViewArrayFactory($interval); - })); - - describe("create", () => { - - it("sanity", () => { - expect(angularViewArrayFactory).toBeDefined(); - }); - - it("returns a ViewArray object", () => { - var viewArray: ViewArrayImpl = angularViewArray.create(); - - expect(viewArray).toBeDefined(); - }); - - describe("returned ViewArray object", () => { - var viewArray: ViewArrayImpl; - - beforeEach(() => { - viewArray = angularViewArray.create(); - }); - - describe("constructor", () => { - // TODO - }); - - describe("length", () => { - - it("returns the number of entries", () => { - viewArray.entries = [{}, {}, {}]; - - expect(viewArray.length()).toEqual(viewArray.entries.length); - }); - }); - - describe("get", () => { - - it("returns the entry at a given index", () => { - var index: number = 8; - viewArray.entries = new Array(10); - viewArray.entries[index] = 3; - - expect(viewArray.get(index)).toEqual(viewArray.entries[index]); - }); - }); - - describe("push", () => { - - it("adds given element to the end of entries", () => { - var element: number = 3; - var originalLength: number = viewArray.length(); - viewArray.push(element); - - expect(viewArray.entries.length).toEqual(originalLength + 1); - expect(viewArray.get(originalLength)).toEqual(element); - }); - - it("sets 'hasEntries' to true", () => { - viewArray.push(2); - - expect(viewArray.hasEntries).toBe(true); - }); - - it("starts timer if 'isVisible' is true", () => { - spyOn(viewArray, "startTimer_").and.returnValue(null); - viewArray.isVisible = true; - viewArray.push(2); - - expect(viewArray.startTimer_).toHaveBeenCalled(); - }); - - it("does not start timer if 'isVisible' is false", () => { - spyOn(viewArray, "startTimer_").and.returnValue(null); - viewArray.isVisible = false; - viewArray.push(2); - - expect(viewArray.startTimer_).not.toHaveBeenCalled(); - }); - }); - - describe("toggle", () => { - - it("sets 'isVisible' to false if currently true", () => { - viewArray.isVisible = true; - viewArray.toggle(); - - expect(viewArray.isVisible).toBe(false); - }); - - it("sets 'isVisible' to true if currently false", () => { - viewArray.isVisible = false; - viewArray.toggle(); - - expect(viewArray.isVisible).toBe(true); - }); - }); - - describe("setVisible", () => { - - it("sets 'isVisible' to false if given false", () => { - viewArray.setVisible(false); - - expect(viewArray.isVisible).toBe(false); - }); - - it("sets 'visibleEntries' to empty array if given false", () => { - viewArray.setVisible(false); - - expect(viewArray.visibleEntries.length).toEqual(0); - }); - - it("shows additional entries if given true", () => { - spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); - viewArray.setVisible(true); - - expect(viewArray.showAdditionalEntries_).toHaveBeenCalled(); - }); - - it("does not show additional entries if given false", () => { - spyOn(viewArray, "showAdditionalEntries_").and.returnValue(null); - viewArray.setVisible(false); - - expect(viewArray.showAdditionalEntries_).not.toHaveBeenCalled(); - }); - - it("starts timer if given true", () => { - spyOn(viewArray, "startTimer_").and.returnValue(null); - viewArray.setVisible(true); - - expect(viewArray.startTimer_).toHaveBeenCalled(); - }); - - it("stops timer if given false", () => { - spyOn(viewArray, "stopTimer_").and.returnValue(null); - viewArray.setVisible(true); - - expect(viewArray.stopTimer_).toHaveBeenCalled(); - }); - }); - }); - }); -}); \ No newline at end of file diff --git a/static/js/services/angular-view-array/angular-view-array.ts b/static/js/services/angular-view-array/angular-view-array.ts deleted file mode 100644 index 0de564392..000000000 --- a/static/js/services/angular-view-array/angular-view-array.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ViewArray } from './view-array'; -import { ViewArrayImpl } from './view-array.impl'; - - -/** - * Specialized wrapper around array which provides a toggle() method for viewing the contents of the - * array in a manner that is asynchronously filled in over a short time period. This prevents long - * pauses in the UI for ngRepeat's when the array is significant in size. - */ - -angularViewArrayFactory.$inject = [ - '$interval' -]; - -export function angularViewArrayFactory($interval): any { - const ADDITIONAL_ENTRIES: number = 20; - - return { - create: function(): ViewArray { - return new ViewArrayImpl($interval, ADDITIONAL_ENTRIES); - } - }; -} - diff --git a/static/js/services/angular-view-array/view-array.ts b/static/js/services/angular-view-array/view-array.ts deleted file mode 100644 index 343ca1efb..000000000 --- a/static/js/services/angular-view-array/view-array.ts +++ /dev/null @@ -1,12 +0,0 @@ -export abstract class ViewArray { - - public abstract length(): number; - - public abstract get(index: number): any; - - public abstract push(elem: any): void; - - public abstract toggle(): void; - - public abstract setVisible(newState: boolean): void; -} \ No newline at end of file diff --git a/static/js/services/table-service.js b/static/js/services/table-service.js index 180cf648a..76968e6c6 100644 --- a/static/js/services/table-service.js +++ b/static/js/services/table-service.js @@ -1,7 +1,7 @@ /** * Service which provides helper methods for constructing and managing tabular data. */ -angular.module('quay').factory('TableService', ['AngularViewArray', function(AngularViewArray) { +angular.module('quay').factory('TableService', ['ViewArrayFactory', function(ViewArrayFactory) { var tableService = {}; tableService.tablePredicateClass = function(name, predicate, reverse) { @@ -31,7 +31,7 @@ angular.module('quay').factory('TableService', ['AngularViewArray', function(Ang }; tableService.buildOrderedItems = function(items, options, filterFields, numericFields, opt_extrafilter) { - var orderedItems = AngularViewArray.create(); + var orderedItems = ViewArrayFactory.create(); items.forEach(function(item) { var filter = options.filter; diff --git a/static/js/services/view-array/view-array.factory.spec.ts b/static/js/services/view-array/view-array.factory.spec.ts new file mode 100644 index 000000000..5e0b11134 --- /dev/null +++ b/static/js/services/view-array/view-array.factory.spec.ts @@ -0,0 +1,27 @@ +import { ViewArrayFactory } from './view-array.factory'; +import { ViewArray } from './view-array'; + + +describe("Factory: ViewArrayFactory", () => { + var viewArrayFactory: ViewArrayFactory; + var $intervalMock: any; + + beforeEach(() => { + $intervalMock = jasmine.createSpy('$intervalSpy'); + $intervalMock.cancel = jasmine.createSpy('cancelSpy'); + viewArrayFactory = new ViewArrayFactory($intervalMock) + }); + + describe("constructor", () => { + + }); + + describe("create", () => { + + it("returns a ViewArray object", () => { + var viewArray: ViewArray = viewArrayFactory.create(); + + expect(viewArray).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/static/js/services/view-array/view-array.factory.ts b/static/js/services/view-array/view-array.factory.ts new file mode 100644 index 000000000..0a4a4de2d --- /dev/null +++ b/static/js/services/view-array/view-array.factory.ts @@ -0,0 +1,29 @@ +import { ViewArray } from './view-array'; +import { ViewArrayImpl } from './view-array.impl'; + + +/** + * Factory for creating ViewArray instances. + */ +export class ViewArrayFactory { + + private ADDITIONAL_ENTRIES: number = 20; + + /** + * @param $interval Angular $interval service. + * @return viewArrayFactory A factory for creating ViewArray objects. + */ + static $inject = ['$interval']; + constructor(private $interval: ng.IIntervalService) { + + } + + /** + * Create a new ViewArray object. + * @return viewArray New ViewArrayImpl instance. + */ + public create(): ViewArray { + return new ViewArrayImpl(this.$interval, this.ADDITIONAL_ENTRIES); + } +} + diff --git a/static/js/services/view-array/view-array.impl.spec.ts b/static/js/services/view-array/view-array.impl.spec.ts new file mode 100644 index 000000000..82128e5bf --- /dev/null +++ b/static/js/services/view-array/view-array.impl.spec.ts @@ -0,0 +1,140 @@ +import { ViewArrayImpl } from './view-array.impl'; + + +describe("ViewArrayImplImpl", () => { + var viewArrayImpl: ViewArrayImpl; + var $intervalMock: any; + var additionalCount: number; + + beforeEach(inject(($injector: ng.auto.IInjectorService) => { + $intervalMock = jasmine.createSpy('$intervalSpy'); + $intervalMock.and.returnValue({}); + $intervalMock.cancel = jasmine.createSpy('cancelSpy'); + additionalCount = 20; + viewArrayImpl = new ViewArrayImpl($intervalMock, additionalCount); + })); + + + describe("constructor", () => { + + it("initializes values", () => { + expect(viewArrayImpl.isVisible).toBe(false); + expect(viewArrayImpl.visibleEntries).toBe(null); + expect(viewArrayImpl.entries.length).toEqual(0); + expect(viewArrayImpl.hasEntries).toBe(false); + expect(viewArrayImpl.hasHiddenEntries).toBe(false); + }); + }); + + describe("length", () => { + + it("returns the number of entries", () => { + viewArrayImpl.entries = [{}, {}, {}]; + + expect(viewArrayImpl.length()).toEqual(viewArrayImpl.entries.length); + }); + }); + + describe("get", () => { + + it("returns the entry at a given index", () => { + var index: number = 8; + viewArrayImpl.entries = new Array(10); + viewArrayImpl.entries[index] = 3; + + expect(viewArrayImpl.get(index)).toEqual(viewArrayImpl.entries[index]); + }); + }); + + describe("push", () => { + + it("adds given element to the end of entries", () => { + var element: number = 3; + var originalLength: number = viewArrayImpl.length(); + viewArrayImpl.push(element); + + expect(viewArrayImpl.entries.length).toEqual(originalLength + 1); + expect(viewArrayImpl.get(originalLength)).toEqual(element); + }); + + it("sets 'hasEntries' to true", () => { + viewArrayImpl.push(2); + + expect(viewArrayImpl.hasEntries).toBe(true); + }); + + it("starts timer if 'isVisible' is true", () => { + viewArrayImpl.isVisible = true; + viewArrayImpl.push(2); + + expect($intervalMock).toHaveBeenCalled(); + }); + + it("does not start timer if 'isVisible' is false", () => { + viewArrayImpl.isVisible = false; + viewArrayImpl.push(2); + + expect($intervalMock).not.toHaveBeenCalled(); + }); + }); + + describe("toggle", () => { + + it("sets 'isVisible' to false if currently true", () => { + viewArrayImpl.isVisible = true; + viewArrayImpl.toggle(); + + expect(viewArrayImpl.isVisible).toBe(false); + }); + + it("sets 'isVisible' to true if currently false", () => { + viewArrayImpl.isVisible = false; + viewArrayImpl.toggle(); + + expect(viewArrayImpl.isVisible).toBe(true); + }); + }); + + describe("setVisible", () => { + + it("sets 'isVisible' to false if given false", () => { + viewArrayImpl.setVisible(false); + + expect(viewArrayImpl.isVisible).toBe(false); + }); + + it("sets 'visibleEntries' to empty array if given false", () => { + viewArrayImpl.setVisible(false); + + expect(viewArrayImpl.visibleEntries.length).toEqual(0); + }); + + it("shows additional entries if given true", () => { + viewArrayImpl.setVisible(true); + }); + + it("does not show additional entries if given false", () => { + viewArrayImpl.setVisible(false); + }); + + it("starts timer if given true", () => { + viewArrayImpl.setVisible(true); + + expect($intervalMock).toHaveBeenCalled(); + }); + + it("does not stop timer if given false and timer is not active", () => { + viewArrayImpl.setVisible(false); + + expect($intervalMock.cancel).not.toHaveBeenCalled(); + }); + + it("stops timer if given false and timer is active", () => { + viewArrayImpl.isVisible = true; + viewArrayImpl.push(2); + viewArrayImpl.setVisible(false); + + expect($intervalMock.cancel).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/static/js/services/angular-view-array/view-array.impl.ts b/static/js/services/view-array/view-array.impl.ts similarity index 55% rename from static/js/services/angular-view-array/view-array.impl.ts rename to static/js/services/view-array/view-array.impl.ts index cca978307..f0ed9a445 100644 --- a/static/js/services/angular-view-array/view-array.impl.ts +++ b/static/js/services/view-array/view-array.impl.ts @@ -3,13 +3,13 @@ import { ViewArray } from './view-array'; export class ViewArrayImpl implements ViewArray { + public entries: any[]; public isVisible: boolean; public visibleEntries: any[]; public hasEntries: boolean; - public entries: any[]; public hasHiddenEntries: boolean; - public timerRef_: any; - public currentIndex_: number; + private timerRef: any; + private currentIndex: number; constructor(private interval: any, private additionalCount: number) { this.isVisible = false; @@ -17,8 +17,8 @@ export class ViewArrayImpl implements ViewArray { this.hasEntries = false; this.entries = []; this.hasHiddenEntries = false; - this.timerRef_ = null; - this.currentIndex_ = 0; + this.timerRef = null; + this.currentIndex = 0; } public length(): number { @@ -34,7 +34,7 @@ export class ViewArrayImpl implements ViewArray { this.hasEntries = true; if (this.isVisible) { - this.startTimer_(); + this.startTimer(); } } @@ -46,45 +46,44 @@ export class ViewArrayImpl implements ViewArray { this.isVisible = newState; this.visibleEntries = []; - this.currentIndex_ = 0; + this.currentIndex = 0; if (newState) { - this.showAdditionalEntries_(); - this.startTimer_(); + this.showAdditionalEntries(); + this.startTimer(); } else { - this.stopTimer_(); + this.stopTimer(); } } - public showAdditionalEntries_(): void { + private showAdditionalEntries(): void { var i: number = 0; - for (i = this.currentIndex_; i < (this.currentIndex_ + this.additionalCount) && i < this.entries.length; ++i) { + for (i = this.currentIndex; i < (this.currentIndex + this.additionalCount) && i < this.entries.length; ++i) { this.visibleEntries.push(this.entries[i]); } - this.currentIndex_ = i; - this.hasHiddenEntries = this.currentIndex_ < this.entries.length; - if (this.currentIndex_ >= this.entries.length) { - this.stopTimer_(); + this.currentIndex = i; + this.hasHiddenEntries = this.currentIndex < this.entries.length; + if (this.currentIndex >= this.entries.length) { + this.stopTimer(); } } - public startTimer_(): void { - if (this.timerRef_) { + private startTimer(): void { + if (this.timerRef) { return; } - var that = this; - this.timerRef_ = this.interval(function() { - that.showAdditionalEntries_(); + this.timerRef = this.interval(() => { + this.showAdditionalEntries(); }, 10); } - public stopTimer_(): void { - if (this.timerRef_) { - this.interval.cancel(this.timerRef_); - this.timerRef_ = null; + private stopTimer(): void { + if (this.timerRef) { + this.interval.cancel(this.timerRef); + this.timerRef = null; } } } \ No newline at end of file diff --git a/static/js/services/view-array/view-array.ts b/static/js/services/view-array/view-array.ts new file mode 100644 index 000000000..fdc89e64a --- /dev/null +++ b/static/js/services/view-array/view-array.ts @@ -0,0 +1,62 @@ +/** + * Specialized wrapper around array which provides a toggle() method for viewing the contents of the + * array in a manner that is asynchronously filled in over a short time period. This prevents long + * pauses in the UI for ngRepeat's when the array is significant in size. + */ +export abstract class ViewArray { + + /** + * The stored entries. + */ + public abstract entries: any; + + /** + * If the entries are displayed. + */ + public abstract isVisible: boolean; + + /** + * The displayed entries. + */ + public abstract visibleEntries: any[]; + + /** + * If there are stored entries. + */ + public abstract hasEntries: boolean; + + /** + * If there are entries not visible. + */ + public abstract hasHiddenEntries: boolean; + + /** + * Get the number of entries stored. + * @return number The number of entries. + */ + public abstract length(): number; + + /** + * Get a specific entry. + * @param index The index of the entry. + * @return element The element at the given index. + */ + public abstract get(index: number): any; + + /** + * Add a new element. + * @param elem The new element. + */ + public abstract push(elem: any): void; + + /** + * Toggle whether the elements are visible. + */ + public abstract toggle(): void; + + /** + * Set whether the elements are visible. + * @param newState True/False if the contents are visible. + */ + public abstract setVisible(newState: boolean): void; +} \ No newline at end of file From edd7314624a1b65516947276ab32e5a5bdc99ba7 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 21 Jan 2017 02:56:29 -0800 Subject: [PATCH 13/19] @Inject decorator for easily annotating dependency-injected class constructors --- .../decorators/inject/inject.decorator.spec.ts | 17 +++++++++++++++++ static/js/decorators/inject/inject.decorator.ts | 11 +++++++++++ .../services/view-array/view-array.factory.ts | 4 ++-- .../services/view-array/view-array.impl.spec.ts | 4 ++-- tsconfig.json | 1 + 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 static/js/decorators/inject/inject.decorator.spec.ts create mode 100644 static/js/decorators/inject/inject.decorator.ts diff --git a/static/js/decorators/inject/inject.decorator.spec.ts b/static/js/decorators/inject/inject.decorator.spec.ts new file mode 100644 index 000000000..2f0a9a17b --- /dev/null +++ b/static/js/decorators/inject/inject.decorator.spec.ts @@ -0,0 +1,17 @@ +import { Inject } from './inject.decorator'; + + +describe("Decorator: Inject", () => { + + describe("parameter injection", () => { + + it("adds given string to the 'inject' property of the annotated class", () => { + expect(ValidService.$inject).toContain('$scope'); + }); + }); +}); + + +class ValidService { + constructor(@Inject('$scope') private $scope: any) {} +} \ No newline at end of file diff --git a/static/js/decorators/inject/inject.decorator.ts b/static/js/decorators/inject/inject.decorator.ts new file mode 100644 index 000000000..2c647a4c7 --- /dev/null +++ b/static/js/decorators/inject/inject.decorator.ts @@ -0,0 +1,11 @@ +/** + * Adds the given value to the inject property of the annotated class. + * Used to annotate the constructor parameters of an AngularJS service/component class. + * @param value The string name of the dependency. + */ +export function Inject(value: string) { + return (target: any, propertyKey: string | symbol, parameterIndex: number): void => { + target.$inject = target.$inject || []; + target.$inject[parameterIndex] = value; + } +} diff --git a/static/js/services/view-array/view-array.factory.ts b/static/js/services/view-array/view-array.factory.ts index 0a4a4de2d..99601e759 100644 --- a/static/js/services/view-array/view-array.factory.ts +++ b/static/js/services/view-array/view-array.factory.ts @@ -1,5 +1,6 @@ import { ViewArray } from './view-array'; import { ViewArrayImpl } from './view-array.impl'; +import { Inject } from '../../decorators/inject/inject.decorator'; /** @@ -13,8 +14,7 @@ export class ViewArrayFactory { * @param $interval Angular $interval service. * @return viewArrayFactory A factory for creating ViewArray objects. */ - static $inject = ['$interval']; - constructor(private $interval: ng.IIntervalService) { + constructor(@Inject('$interval') private $interval: ng.IIntervalService) { } diff --git a/static/js/services/view-array/view-array.impl.spec.ts b/static/js/services/view-array/view-array.impl.spec.ts index 82128e5bf..ae9ee113b 100644 --- a/static/js/services/view-array/view-array.impl.spec.ts +++ b/static/js/services/view-array/view-array.impl.spec.ts @@ -6,13 +6,13 @@ describe("ViewArrayImplImpl", () => { var $intervalMock: any; var additionalCount: number; - beforeEach(inject(($injector: ng.auto.IInjectorService) => { + beforeEach(() => { $intervalMock = jasmine.createSpy('$intervalSpy'); $intervalMock.and.returnValue({}); $intervalMock.cancel = jasmine.createSpy('cancelSpy'); additionalCount = 20; viewArrayImpl = new ViewArrayImpl($intervalMock, additionalCount); - })); + }); describe("constructor", () => { diff --git a/tsconfig.json b/tsconfig.json index 2e7ab719c..9ec73f044 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "module": "commonjs", "outDir": "./build/", "target": "es5", + "experimentalDecorators": true, "sourceMap": true, "paths": { "sass/*": ["./static/css/directives/components/pages/*"] From 2a59014f0bb3648cb3d20cc24d63a8e36ad0b540 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 21 Jan 2017 22:09:22 -0800 Subject: [PATCH 14/19] refactoring RouteBuilderFactory into service --- static/js/quay.module.ts | 4 +-- static/js/quay.routes.ts | 9 ++--- .../route-builder.factory.spec.ts | 35 +++++++++++++++++++ .../route-builder/route-builder.factory.ts | 14 ++++++++ .../route-builder.service.impl.ts | 0 .../route-builder.service.spec.ts | 0 .../route-builder/route-builder.service.ts | 0 7 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 static/js/services/route-builder/route-builder.factory.spec.ts create mode 100644 static/js/services/route-builder/route-builder.factory.ts rename static/js/{ => services}/route-builder/route-builder.service.impl.ts (100%) rename static/js/{ => services}/route-builder/route-builder.service.spec.ts (100%) rename static/js/{ => services}/route-builder/route-builder.service.ts (100%) diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index b61c0a4d4..5a6d1d538 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -3,7 +3,7 @@ import { quayConfig } from './quay.config'; import quayPages from './quay-pages.module'; import quayRun from './quay.run'; import { ViewArrayFactory } from './services/view-array/view-array.factory'; -import { routeBuilderFactory } from './route-builder/route-builder.service.impl'; +import RouteBuilderFactory from './services/route-builder/route-builder.factory'; import NAME_PATTERNS from './constants/name-patterns.constant'; import { routeConfig } from './quay.routes'; import { CONFIG } from './constants/quay-config.constant'; @@ -51,7 +51,7 @@ export default angular .config(routeConfig) .constant('NAME_PATTERNS', NAME_PATTERNS) .constant('CONFIG', CONFIG) - .factory('RouteBuilder', routeBuilderFactory) + .service('RouteBuilderFactory', RouteBuilderFactory) .service('ViewArrayFactory', ViewArrayFactory) .run(quayRun) .name; \ No newline at end of file diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts index e74a735b7..f47b08d26 100644 --- a/static/js/quay.routes.ts +++ b/static/js/quay.routes.ts @@ -1,17 +1,18 @@ -import { RouteBuilder } from './route-builder/route-builder.service'; +import RouteBuilderFactory from './services/route-builder/route-builder.factory'; +import { RouteBuilder } from './services/route-builder/route-builder.service'; import pages from './constants/pages.constant'; routeConfig.$inject = [ 'pages', - 'RouteBuilderProvider', + 'RouteBuilderFactoryProvider', '$routeProvider', '$locationProvider', ]; export function routeConfig( pages: any, - RouteBuilderProvider: any, + RouteBuilderFactoryProvider: any, $routeProvider: ng.route.IRouteProvider, $locationProvider: ng.ILocationProvider) { $locationProvider.html5Mode(true); @@ -23,7 +24,7 @@ export function routeConfig( var layoutProfile: string = 'layout'; - var routeBuilder: RouteBuilder = new RouteBuilderProvider.$get()($routeProvider, pages, [ + var routeBuilder: RouteBuilder = RouteBuilderFactoryProvider.$get().create($routeProvider, pages, [ // Start with the old pages (if we asked for it). {id: 'old-layout', templatePath: '/static/partials/'}, diff --git a/static/js/services/route-builder/route-builder.factory.spec.ts b/static/js/services/route-builder/route-builder.factory.spec.ts new file mode 100644 index 000000000..27ab51dd8 --- /dev/null +++ b/static/js/services/route-builder/route-builder.factory.spec.ts @@ -0,0 +1,35 @@ +import RouteBuilderFactory from './route-builder.factory'; +import { RouteBuilder } from "static/js/services/route-builder/route-builder.service"; + + +describe("Factory: RouteBuilderFactory", () => { + var routeBuilderFactory: RouteBuilderFactory; + var routeProviderMock; + var pagesMock; + var profiles; + var currentProfile; + + beforeEach(() => { + profiles = [ + {id: 'old-layout', templatePath: '/static/partials/'}, + {id: 'layout', templatePath: '/static/partials/'} + ]; + currentProfile = 'layout'; + routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']); + pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']); + routeBuilderFactory = new RouteBuilderFactory(); + }); + + describe("constructor", () => { + + }); + + describe("create", () => { + + it("returns a RouteBuilder instance", () => { + var routeBuilder: RouteBuilder = routeBuilderFactory.create(routeProviderMock, pagesMock, profiles, currentProfile); + + expect(routeBuilder).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/static/js/services/route-builder/route-builder.factory.ts b/static/js/services/route-builder/route-builder.factory.ts new file mode 100644 index 000000000..4f9750b11 --- /dev/null +++ b/static/js/services/route-builder/route-builder.factory.ts @@ -0,0 +1,14 @@ +import { RouteBuilder } from './route-builder.service'; +import { RouteBuilderImpl } from './route-builder.service.impl'; + + +export default class RouteBuilderFactory { + + constructor() { + + } + + public create(routeProvider, pages, profiles, currentProfile): RouteBuilder { + return new RouteBuilderImpl(routeProvider, pages, profiles, currentProfile); + } +} diff --git a/static/js/route-builder/route-builder.service.impl.ts b/static/js/services/route-builder/route-builder.service.impl.ts similarity index 100% rename from static/js/route-builder/route-builder.service.impl.ts rename to static/js/services/route-builder/route-builder.service.impl.ts diff --git a/static/js/route-builder/route-builder.service.spec.ts b/static/js/services/route-builder/route-builder.service.spec.ts similarity index 100% rename from static/js/route-builder/route-builder.service.spec.ts rename to static/js/services/route-builder/route-builder.service.spec.ts diff --git a/static/js/route-builder/route-builder.service.ts b/static/js/services/route-builder/route-builder.service.ts similarity index 100% rename from static/js/route-builder/route-builder.service.ts rename to static/js/services/route-builder/route-builder.service.ts From 64a4b682160e5d510e6005ab91889deb57102221 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Mon, 23 Jan 2017 01:57:00 -0800 Subject: [PATCH 15/19] removed unnecessary factory classes, simplified existing services --- static/js/directives/ui/build-logs-view.js | 4 +- static/js/directives/ui/image-feature-view.js | 2 +- .../directives/ui/image-vulnerability-view.js | 2 +- static/js/quay.module.ts | 6 +-- static/js/quay.routes.ts | 14 +------ .../route-builder.factory.spec.ts | 35 ---------------- .../route-builder/route-builder.factory.ts | 14 ------- .../route-builder.service.impl.ts | 20 +++++---- .../route-builder.service.spec.ts | 42 +++++++++++-------- static/js/services/table-service.js | 4 +- .../view-array/view-array.factory.spec.ts | 27 ------------ .../services/view-array/view-array.factory.ts | 29 ------------- .../view-array/view-array.impl.spec.ts | 13 ++++-- .../js/services/view-array/view-array.impl.ts | 8 +++- static/js/services/view-array/view-array.ts | 7 ++++ 15 files changed, 69 insertions(+), 158 deletions(-) delete mode 100644 static/js/services/route-builder/route-builder.factory.spec.ts delete mode 100644 static/js/services/route-builder/route-builder.factory.ts delete mode 100644 static/js/services/view-array/view-array.factory.spec.ts delete mode 100644 static/js/services/view-array/view-array.factory.ts diff --git a/static/js/directives/ui/build-logs-view.js b/static/js/directives/ui/build-logs-view.js index e26d10b2b..2d117bb9d 100644 --- a/static/js/directives/ui/build-logs-view.js +++ b/static/js/directives/ui/build-logs-view.js @@ -13,7 +13,7 @@ angular.module('quay').directive('buildLogsView', function () { 'useTimestamps': '=useTimestamps', 'buildUpdated': '&buildUpdated' }, - controller: function($scope, $element, $interval, $sanitize, ansi2html, ViewArrayFactory, + controller: function($scope, $element, $interval, $sanitize, ansi2html, ViewArray, AngularPollChannel, ApiService, Restangular, UtilService) { var result = $element.find('#copyButton').clipboardCopy(); @@ -53,7 +53,7 @@ angular.module('quay').directive('buildLogsView', function () { var entry = logs[i]; var type = entry['type'] || 'entry'; if (type == 'command' || type == 'phase' || type == 'error') { - entry['logs'] = ViewArrayFactory.create(); + entry['logs'] = ViewArray.create(); entry['index'] = $scope.logStartIndex + i; $scope.logEntries.push(entry); diff --git a/static/js/directives/ui/image-feature-view.js b/static/js/directives/ui/image-feature-view.js index 484bfe8f9..cbf27fbca 100644 --- a/static/js/directives/ui/image-feature-view.js +++ b/static/js/directives/ui/image-feature-view.js @@ -13,7 +13,7 @@ angular.module('quay').directive('imageFeatureView', function () { 'image': '=image', 'isEnabled': '=isEnabled' }, - controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArrayFactory, ImageMetadataService, TableService) { + controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, ImageMetadataService, TableService) { $scope.options = { 'filter': null, 'predicate': 'fixableScore', diff --git a/static/js/directives/ui/image-vulnerability-view.js b/static/js/directives/ui/image-vulnerability-view.js index 25d49bb63..ca6f792aa 100644 --- a/static/js/directives/ui/image-vulnerability-view.js +++ b/static/js/directives/ui/image-vulnerability-view.js @@ -13,7 +13,7 @@ angular.module('quay').directive('imageVulnerabilityView', function () { 'image': '=image', 'isEnabled': '=isEnabled' }, - controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArrayFactory, ImageMetadataService, TableService) { + controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, ImageMetadataService, TableService) { $scope.options = { 'filter': null, 'fixableVulns': false, diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 5a6d1d538..01d7fce35 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -2,8 +2,7 @@ import * as angular from 'angular'; import { quayConfig } from './quay.config'; import quayPages from './quay-pages.module'; import quayRun from './quay.run'; -import { ViewArrayFactory } from './services/view-array/view-array.factory'; -import RouteBuilderFactory from './services/route-builder/route-builder.factory'; +import { ViewArrayImpl } from './services/view-array/view-array.impl'; import NAME_PATTERNS from './constants/name-patterns.constant'; import { routeConfig } from './quay.routes'; import { CONFIG } from './constants/quay-config.constant'; @@ -51,7 +50,6 @@ export default angular .config(routeConfig) .constant('NAME_PATTERNS', NAME_PATTERNS) .constant('CONFIG', CONFIG) - .service('RouteBuilderFactory', RouteBuilderFactory) - .service('ViewArrayFactory', ViewArrayFactory) + .service('ViewArray', ViewArrayImpl) .run(quayRun) .name; \ No newline at end of file diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts index f47b08d26..0f94f475a 100644 --- a/static/js/quay.routes.ts +++ b/static/js/quay.routes.ts @@ -1,18 +1,16 @@ -import RouteBuilderFactory from './services/route-builder/route-builder.factory'; +import { RouteBuilderImpl } from './services/route-builder/route-builder.service.impl'; import { RouteBuilder } from './services/route-builder/route-builder.service'; import pages from './constants/pages.constant'; routeConfig.$inject = [ 'pages', - 'RouteBuilderFactoryProvider', '$routeProvider', '$locationProvider', ]; export function routeConfig( pages: any, - RouteBuilderFactoryProvider: any, $routeProvider: ng.route.IRouteProvider, $locationProvider: ng.ILocationProvider) { $locationProvider.html5Mode(true); @@ -22,15 +20,7 @@ export function routeConfig( // index rule to make sure that deep links directly deep into the app continue to work. // WARNING WARNING WARNING - var layoutProfile: string = 'layout'; - - var routeBuilder: RouteBuilder = RouteBuilderFactoryProvider.$get().create($routeProvider, pages, [ - // Start with the old pages (if we asked for it). - {id: 'old-layout', templatePath: '/static/partials/'}, - - // Fallback back combined new/existing pages. - {id: 'layout', templatePath: '/static/partials/'} - ], layoutProfile); + var routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pages); if ((window).__features.SUPER_USERS) { // QE Management diff --git a/static/js/services/route-builder/route-builder.factory.spec.ts b/static/js/services/route-builder/route-builder.factory.spec.ts deleted file mode 100644 index 27ab51dd8..000000000 --- a/static/js/services/route-builder/route-builder.factory.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import RouteBuilderFactory from './route-builder.factory'; -import { RouteBuilder } from "static/js/services/route-builder/route-builder.service"; - - -describe("Factory: RouteBuilderFactory", () => { - var routeBuilderFactory: RouteBuilderFactory; - var routeProviderMock; - var pagesMock; - var profiles; - var currentProfile; - - beforeEach(() => { - profiles = [ - {id: 'old-layout', templatePath: '/static/partials/'}, - {id: 'layout', templatePath: '/static/partials/'} - ]; - currentProfile = 'layout'; - routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']); - pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']); - routeBuilderFactory = new RouteBuilderFactory(); - }); - - describe("constructor", () => { - - }); - - describe("create", () => { - - it("returns a RouteBuilder instance", () => { - var routeBuilder: RouteBuilder = routeBuilderFactory.create(routeProviderMock, pagesMock, profiles, currentProfile); - - expect(routeBuilder).toBeDefined(); - }); - }); -}); \ No newline at end of file diff --git a/static/js/services/route-builder/route-builder.factory.ts b/static/js/services/route-builder/route-builder.factory.ts deleted file mode 100644 index 4f9750b11..000000000 --- a/static/js/services/route-builder/route-builder.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RouteBuilder } from './route-builder.service'; -import { RouteBuilderImpl } from './route-builder.service.impl'; - - -export default class RouteBuilderFactory { - - constructor() { - - } - - public create(routeProvider, pages, profiles, currentProfile): RouteBuilder { - return new RouteBuilderImpl(routeProvider, pages, profiles, currentProfile); - } -} 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 8f6382abb..12780952a 100644 --- a/static/js/services/route-builder/route-builder.service.impl.ts +++ b/static/js/services/route-builder/route-builder.service.impl.ts @@ -1,18 +1,20 @@ import { RouteBuilder } from './route-builder.service'; -export function routeBuilderFactory() { - return (routeProvider, pages, profiles, currentProfile): RouteBuilder => { - return new RouteBuilderImpl(routeProvider, pages, profiles, currentProfile); - } -} - - export class RouteBuilderImpl implements RouteBuilder { - constructor(private routeProvider, private pages, public profiles, currentProfile) { + public currentProfile: string = 'layout'; + public profiles: any[] = [ + // Start with the old pages (if we asked for it). + {id: 'old-layout', templatePath: '/static/partials/'}, + // Fallback back combined new/existing pages. + {id: 'layout', templatePath: '/static/partials/'} + ]; + + + constructor(private routeProvider: ng.route.IRouteProvider, private pages: any) { for (let i = 0; i < this.profiles.length; ++i) { - if (this.profiles[i].id == currentProfile) { + if (this.profiles[i].id == this.currentProfile) { this.profiles = this.profiles.slice(i); break; } 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 ef29b5ba4..fd09a277a 100644 --- a/static/js/services/route-builder/route-builder.service.spec.ts +++ b/static/js/services/route-builder/route-builder.service.spec.ts @@ -2,17 +2,15 @@ import { RouteBuilderImpl } from './route-builder.service.impl'; describe("Service: RouteBuilderImpl", () => { - var routeProviderMock; - var pagesMock; - var profiles; - var currentProfile; + var routeProviderMock: any; + var pagesMock: any; + var profiles: any[]; beforeEach((() => { profiles = [ {id: 'old-layout', templatePath: '/static/partials/'}, {id: 'layout', templatePath: '/static/partials/'} ]; - currentProfile = 'layout'; routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']); pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']); })); @@ -20,29 +18,37 @@ describe("Service: RouteBuilderImpl", () => { describe("constructor", () => { it("returns a RouteBuilder object", () => { - var routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock); expect(routeBuilder).toBeDefined(); }); - it("sets 'profiles' to all given profiles if given current profile does not match any of the given profiles' id", () => { - var routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, 'fake-profile'); + it("initializes current profile to 'layout'", () => { + var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock); - expect(routeBuilder.profiles).toEqual(profiles); + expect(routeBuilder.currentProfile).toEqual('layout'); + }); + + it("initializes available profiles", () => { + var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock); + var matchingRoutes: any[] = routeBuilder.profiles.filter((profile) => { + return profiles.indexOf(profile) == -1; + }); + expect(matchingRoutes).toEqual(routeBuilder.profiles); }); it("sets 'profiles' to the first given profile with id matching given current profile", () => { - var routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock); expect(routeBuilder.profiles).toEqual([profiles[1]]); }); }); describe("otherwise", () => { - var routeBuilder; + var routeBuilder: RouteBuilderImpl; beforeEach(() => { - routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock); }); it("calls routeProvider to set fallback route with given options", () => { @@ -54,10 +60,10 @@ describe("Service: RouteBuilderImpl", () => { }); describe("route", () => { - var routeBuilder; - var path; - var pagename; - var page; + var routeBuilder: RouteBuilderImpl; + var path: string; + var pagename: string; + var page: any; beforeEach(() => { path = '/repository/:namespace/:name'; @@ -68,7 +74,7 @@ describe("Service: RouteBuilderImpl", () => { controller: jasmine.createSpy('pageController'), flags: {}, }; - routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock, profiles, currentProfile); + routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock); }); it("calls pages with given pagename and 'profiles' to get matching page and profile pair", () => { @@ -91,7 +97,7 @@ describe("Service: RouteBuilderImpl", () => { it("calls routeProvider to set route for given path and options", () => { pagesMock.get.and.returnValue([profiles[1], page]); - var expectedOptions = { + var expectedOptions: any = { templateUrl: profiles[1].templatePath + page.templateName, reloadOnSearch: false, controller: page.controller, diff --git a/static/js/services/table-service.js b/static/js/services/table-service.js index 76968e6c6..4975ad1fd 100644 --- a/static/js/services/table-service.js +++ b/static/js/services/table-service.js @@ -1,7 +1,7 @@ /** * Service which provides helper methods for constructing and managing tabular data. */ -angular.module('quay').factory('TableService', ['ViewArrayFactory', function(ViewArrayFactory) { +angular.module('quay').factory('TableService', ['ViewArray', function(ViewArray) { var tableService = {}; tableService.tablePredicateClass = function(name, predicate, reverse) { @@ -31,7 +31,7 @@ angular.module('quay').factory('TableService', ['ViewArrayFactory', function(Vie }; tableService.buildOrderedItems = function(items, options, filterFields, numericFields, opt_extrafilter) { - var orderedItems = ViewArrayFactory.create(); + var orderedItems = ViewArray.create(); items.forEach(function(item) { var filter = options.filter; diff --git a/static/js/services/view-array/view-array.factory.spec.ts b/static/js/services/view-array/view-array.factory.spec.ts deleted file mode 100644 index 5e0b11134..000000000 --- a/static/js/services/view-array/view-array.factory.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ViewArrayFactory } from './view-array.factory'; -import { ViewArray } from './view-array'; - - -describe("Factory: ViewArrayFactory", () => { - var viewArrayFactory: ViewArrayFactory; - var $intervalMock: any; - - beforeEach(() => { - $intervalMock = jasmine.createSpy('$intervalSpy'); - $intervalMock.cancel = jasmine.createSpy('cancelSpy'); - viewArrayFactory = new ViewArrayFactory($intervalMock) - }); - - describe("constructor", () => { - - }); - - describe("create", () => { - - it("returns a ViewArray object", () => { - var viewArray: ViewArray = viewArrayFactory.create(); - - expect(viewArray).toBeDefined(); - }); - }); -}); \ No newline at end of file diff --git a/static/js/services/view-array/view-array.factory.ts b/static/js/services/view-array/view-array.factory.ts deleted file mode 100644 index 99601e759..000000000 --- a/static/js/services/view-array/view-array.factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ViewArray } from './view-array'; -import { ViewArrayImpl } from './view-array.impl'; -import { Inject } from '../../decorators/inject/inject.decorator'; - - -/** - * Factory for creating ViewArray instances. - */ -export class ViewArrayFactory { - - private ADDITIONAL_ENTRIES: number = 20; - - /** - * @param $interval Angular $interval service. - * @return viewArrayFactory A factory for creating ViewArray objects. - */ - constructor(@Inject('$interval') private $interval: ng.IIntervalService) { - - } - - /** - * Create a new ViewArray object. - * @return viewArray New ViewArrayImpl instance. - */ - public create(): ViewArray { - return new ViewArrayImpl(this.$interval, this.ADDITIONAL_ENTRIES); - } -} - diff --git a/static/js/services/view-array/view-array.impl.spec.ts b/static/js/services/view-array/view-array.impl.spec.ts index ae9ee113b..75163a9d6 100644 --- a/static/js/services/view-array/view-array.impl.spec.ts +++ b/static/js/services/view-array/view-array.impl.spec.ts @@ -4,14 +4,12 @@ import { ViewArrayImpl } from './view-array.impl'; describe("ViewArrayImplImpl", () => { var viewArrayImpl: ViewArrayImpl; var $intervalMock: any; - var additionalCount: number; beforeEach(() => { $intervalMock = jasmine.createSpy('$intervalSpy'); $intervalMock.and.returnValue({}); $intervalMock.cancel = jasmine.createSpy('cancelSpy'); - additionalCount = 20; - viewArrayImpl = new ViewArrayImpl($intervalMock, additionalCount); + viewArrayImpl = new ViewArrayImpl($intervalMock); }); @@ -137,4 +135,13 @@ describe("ViewArrayImplImpl", () => { expect($intervalMock.cancel).toHaveBeenCalled(); }); }); + + describe("create", () => { + + it("returns a new ViewArrayImpl instance", () => { + var newViewArrayImpl: ViewArrayImpl = viewArrayImpl.create(); + + expect(newViewArrayImpl).toBeDefined(); + }); + }); }); \ No newline at end of file diff --git a/static/js/services/view-array/view-array.impl.ts b/static/js/services/view-array/view-array.impl.ts index f0ed9a445..8cc3f75c4 100644 --- a/static/js/services/view-array/view-array.impl.ts +++ b/static/js/services/view-array/view-array.impl.ts @@ -1,4 +1,5 @@ import { ViewArray } from './view-array'; +import { Inject } from '../../decorators/inject/inject.decorator'; export class ViewArrayImpl implements ViewArray { @@ -10,8 +11,9 @@ export class ViewArrayImpl implements ViewArray { public hasHiddenEntries: boolean; private timerRef: any; private currentIndex: number; + private additionalCount: number = 20; - constructor(private interval: any, private additionalCount: number) { + constructor(@Inject('$interval') private interval: any) { this.isVisible = false; this.visibleEntries = null; this.hasEntries = false; @@ -57,6 +59,10 @@ export class ViewArrayImpl implements ViewArray { } } + public create(): ViewArrayImpl { + return new ViewArrayImpl(this.interval); + } + private showAdditionalEntries(): void { var i: number = 0; for (i = this.currentIndex; i < (this.currentIndex + this.additionalCount) && i < this.entries.length; ++i) { diff --git a/static/js/services/view-array/view-array.ts b/static/js/services/view-array/view-array.ts index fdc89e64a..fbdfaf4db 100644 --- a/static/js/services/view-array/view-array.ts +++ b/static/js/services/view-array/view-array.ts @@ -1,3 +1,4 @@ +import { ViewArrayImpl } from "static/js/services/view-array/view-array.impl"; /** * Specialized wrapper around array which provides a toggle() method for viewing the contents of the * array in a manner that is asynchronously filled in over a short time period. This prevents long @@ -59,4 +60,10 @@ export abstract class ViewArray { * @param newState True/False if the contents are visible. */ public abstract setVisible(newState: boolean): void; + + /** + * Factory function to create a new ViewArray. + * @return viewArray New ViewArray instance. + */ + public abstract create(): ViewArrayImpl; } \ No newline at end of file From 6b2222a3ec6fe521d37728a1415c90df9bac6217 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Mon, 23 Jan 2017 12:34:22 -0800 Subject: [PATCH 16/19] renamed properties placed on the window object to INJECTED_ --- static/js/constants/injected-values.constant.ts | 16 ++++++++++++++++ static/js/constants/quay-config.constant.ts | 10 ---------- static/js/quay.config.ts | 10 +++++----- static/js/quay.module.ts | 16 +++++++++------- static/js/quay.routes.ts | 6 ++++-- static/js/quay.run.ts | 6 +++--- 6 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 static/js/constants/injected-values.constant.ts delete mode 100644 static/js/constants/quay-config.constant.ts diff --git a/static/js/constants/injected-values.constant.ts b/static/js/constants/injected-values.constant.ts new file mode 100644 index 000000000..dcfbcbd32 --- /dev/null +++ b/static/js/constants/injected-values.constant.ts @@ -0,0 +1,16 @@ +/** + * Configuration data set. + */ +export const INJECTED_CONFIG: any = (window).__config; + + +/** + * REST API route information. + */ +export const INJECTED_ENDPOINTS: any = (window).__endpoints; + + +/** + * Features information. + */ +export const INJECTED_FEATURES: any = (window).__features; \ No newline at end of file diff --git a/static/js/constants/quay-config.constant.ts b/static/js/constants/quay-config.constant.ts deleted file mode 100644 index 400207b75..000000000 --- a/static/js/constants/quay-config.constant.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Configuration data set. - */ -export const CONFIG: any = (window).__config; - - -/** - * REST API route information. - */ -export const ENDPOINTS: any = (window).__endpoints; \ No newline at end of file diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts index f7ba10686..9143c31e7 100644 --- a/static/js/quay.config.ts +++ b/static/js/quay.config.ts @@ -3,7 +3,7 @@ import * as Raven from 'raven-js'; quayConfig.$inject = [ '$provide', - 'CONFIG', + 'INJECTED_CONFIG', 'cfpLoadingBarProvider', '$tooltipProvider', '$compileProvider', @@ -13,7 +13,7 @@ quayConfig.$inject = [ export function quayConfig( $provide, - CONFIG, + INJECTED_CONFIG, cfpLoadingBarProvider, $tooltipProvider, $compileProvider, @@ -40,7 +40,7 @@ export function quayConfig( return tooltipFactory.apply(this, arguments); }; - if (!CONFIG['DEBUG']) { + if (!INJECTED_CONFIG['DEBUG']) { $compileProvider.debugInfoEnabled(false); } @@ -52,12 +52,12 @@ export function quayConfig( RestangularProvider.setBaseUrl('/api/v1/'); // Configure analytics. - if (CONFIG && CONFIG.MIXPANEL_KEY) { + if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { $analyticsProvider.virtualPageviews(true); } // Configure sentry. - if (CONFIG && CONFIG.SENTRY_PUBLIC_DSN) { + if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { $provide.decorator("$exceptionHandler", function($delegate) { return function(ex, cause) { $delegate(ex, cause); diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 01d7fce35..b5d17353b 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -5,7 +5,7 @@ import quayRun from './quay.run'; import { ViewArrayImpl } from './services/view-array/view-array.impl'; import NAME_PATTERNS from './constants/name-patterns.constant'; import { routeConfig } from './quay.routes'; -import { CONFIG } from './constants/quay-config.constant'; +import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from './constants/injected-values.constant'; var quayDependencies: string[] = [ @@ -28,19 +28,19 @@ var quayDependencies: string[] = [ 'react' ]; -if (CONFIG && (CONFIG.MIXPANEL_KEY || CONFIG.MUNCHKIN_KEY || CONFIG.GOOGLE_ANALYTICS_KEY)) { +if (INJECTED_CONFIG && (INJECTED_CONFIG.MIXPANEL_KEY || INJECTED_CONFIG.MUNCHKIN_KEY || INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY)) { quayDependencies.push('angulartics'); } -if (CONFIG && CONFIG.MIXPANEL_KEY) { +if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { quayDependencies.push('angulartics.mixpanel'); } -if (CONFIG && CONFIG.MUNCHKIN_KEY) { +if (INJECTED_CONFIG && INJECTED_CONFIG.MUNCHKIN_KEY) { quayDependencies.push('angulartics.marketo'); } -if (CONFIG && CONFIG.GOOGLE_ANALYTICS_KEY) { +if (INJECTED_CONFIG && INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY) { quayDependencies.push('angulartics.google.analytics'); } -if (CONFIG && CONFIG.RECAPTCHA_SITE_KEY) { +if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { quayDependencies.push('vcRecaptcha'); } @@ -49,7 +49,9 @@ export default angular .config(quayConfig) .config(routeConfig) .constant('NAME_PATTERNS', NAME_PATTERNS) - .constant('CONFIG', CONFIG) + .constant('INJECTED_CONFIG', INJECTED_CONFIG) + .constant('INJECTED_FEATURES', INJECTED_FEATURES) + .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS) .service('ViewArray', ViewArrayImpl) .run(quayRun) .name; \ No newline at end of file diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts index 0f94f475a..e458e5715 100644 --- a/static/js/quay.routes.ts +++ b/static/js/quay.routes.ts @@ -7,12 +7,14 @@ routeConfig.$inject = [ 'pages', '$routeProvider', '$locationProvider', + 'INJECTED_FEATURES', ]; export function routeConfig( pages: any, $routeProvider: ng.route.IRouteProvider, - $locationProvider: ng.ILocationProvider) { + $locationProvider: ng.ILocationProvider, + INJECTED_FEATURES) { $locationProvider.html5Mode(true); // WARNING WARNING WARNING @@ -22,7 +24,7 @@ export function routeConfig( var routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pages); - if ((window).__features.SUPER_USERS) { + if (INJECTED_FEATURES.SUPER_USERS) { // QE Management routeBuilder.route('/superuser/', 'superuser') // QE Setup diff --git a/static/js/quay.run.ts b/static/js/quay.run.ts index e7513c560..ac28801b6 100644 --- a/static/js/quay.run.ts +++ b/static/js/quay.run.ts @@ -14,7 +14,7 @@ quayRun.$inject = [ '$anchorScroll', 'UtilService', 'MetaService', - 'CONFIG', + 'INJECTED_CONFIG', ]; export default function quayRun( @@ -30,8 +30,8 @@ export default function quayRun( $anchorScroll, UtilService, MetaService, - CONFIG) { - var defaultTitle = CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; + INJECTED_CONFIG) { + var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; // Handle session security. Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], {'_csrf_token': (window).__token || ''}); From c9fa22b093084259cdeefccfc068f8c784a6ccdf Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Tue, 24 Jan 2017 14:05:06 -0800 Subject: [PATCH 17/19] moved Webpack bundle directory out of /static/js because it contains more than just JS files --- .gitignore | 2 +- endpoints/common.py | 6 ++---- grunt/Gruntfile.js | 9 +++++++-- .../directives/components/pages/repo-page/main.tsx | 2 +- .../services/route-builder/route-builder.service.ts | 12 ++++++++++++ webpack.config.js | 2 +- 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 1a5d2c3a8..9c8de6681 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ dest node_modules static/ldn static/fonts -static/js/build +static/build stack_local test/data/registry/ typings diff --git a/endpoints/common.py b/endpoints/common.py index 322bc2427..e73fa97da 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -161,14 +161,12 @@ def render_page_template(name, route_data=None, **kwargs): library_styles = list_files('lib', 'css') main_styles = list_files('css', 'css') library_scripts = list_files('lib', 'js') - main_scripts = list_files('js', 'js') + # Ensure Webpack bundle is first script on page + main_scripts = ['build/bundle.js'] + list_files('js', 'js') file_lists = [library_styles, main_styles, library_scripts, main_scripts] for file_list in file_lists: file_list.sort() - # Ensure Webpack bundle is first script on page - if 'js/build/bundle.js' in main_scripts: main_scripts.remove('js/build/bundle.js') - main_scripts = ['js/build/bundle.js'] + main_scripts else: library_styles = [] main_styles = ['dist/quay-frontend.css'] diff --git a/grunt/Gruntfile.js b/grunt/Gruntfile.js index ef7a4a6f9..d56d0fb51 100644 --- a/grunt/Gruntfile.js +++ b/grunt/Gruntfile.js @@ -25,8 +25,13 @@ module.exports = function(grunt) { }, }, build: { - src: ['../static/lib/**/*.js', '../static/js/**/*.js', '../static/dist/template-cache.js', - '!../static/js/**/*.spec.js'], + src: [ + '../static/lib/**/*.js', + '../static/js/**/*.js', + '../static/build/*.js', + '../static/dist/template-cache.js', + '!../static/js/**/*.spec.js' + ], dest: '../static/dist/<%= pkg.name %>.js' } }, diff --git a/static/js/directives/components/pages/repo-page/main.tsx b/static/js/directives/components/pages/repo-page/main.tsx index 094381f92..2a07a4421 100644 --- a/static/js/directives/components/pages/repo-page/main.tsx +++ b/static/js/directives/components/pages/repo-page/main.tsx @@ -1,4 +1,4 @@ -import "../../../../../css/directives/components/pages/repo-page/repo-page.scss"; +import "sass/repo-page/repo-page.scss"; import repoHeader from "./header"; import repoSidebar from "./sidebar"; import repoBody from "./body"; diff --git a/static/js/services/route-builder/route-builder.service.ts b/static/js/services/route-builder/route-builder.service.ts index 106f03ba1..0d99ab075 100644 --- a/static/js/services/route-builder/route-builder.service.ts +++ b/static/js/services/route-builder/route-builder.service.ts @@ -1,6 +1,18 @@ +/** + * Constructs client-side routes. + */ export abstract class RouteBuilder { + /** + * Configure the redirect route. + * @param options Configuration options. + */ public abstract otherwise(options: any): void; + /** + * Register a route. + * @param path The URL of the route. + * @param pagename The name of the page to associate with this route. + */ public abstract route(path: string, pagename: string): RouteBuilder; } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index be272f0a3..0e439c03f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ var path = require("path"); var config = { entry: "./static/js/quay.module.ts", output: { - path: path.resolve(__dirname, "static/js/build"), + path: path.resolve(__dirname, "static/build"), filename: "bundle.js" }, resolve: { From 43f95c52a0896f97b14b4c9504965029166073a0 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 25 Jan 2017 16:11:19 -0800 Subject: [PATCH 18/19] retrieve if needed --- grunt/Gruntfile.js | 2 +- static/js/quay.config.ts | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/grunt/Gruntfile.js b/grunt/Gruntfile.js index d56d0fb51..29ead6454 100644 --- a/grunt/Gruntfile.js +++ b/grunt/Gruntfile.js @@ -27,8 +27,8 @@ module.exports = function(grunt) { build: { src: [ '../static/lib/**/*.js', - '../static/js/**/*.js', '../static/build/*.js', + '../static/js/**/*.js', '../static/dist/template-cache.js', '!../static/js/**/*.spec.js' ], diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts index 9143c31e7..fc7fc7293 100644 --- a/static/js/quay.config.ts +++ b/static/js/quay.config.ts @@ -3,29 +3,29 @@ import * as Raven from 'raven-js'; quayConfig.$inject = [ '$provide', + '$injector', 'INJECTED_CONFIG', 'cfpLoadingBarProvider', '$tooltipProvider', '$compileProvider', 'RestangularProvider', - '$analyticsProvider', ]; export function quayConfig( - $provide, - INJECTED_CONFIG, - cfpLoadingBarProvider, - $tooltipProvider, - $compileProvider, - RestangularProvider, - $analyticsProvider) { + $provide: ng.auto.IProvideService, + $injector: ng.auto.IInjectorService, + INJECTED_CONFIG: any, + cfpLoadingBarProvider: any, + $tooltipProvider: any, + $compileProvider: ng.ICompileProvider, + RestangularProvider: any) { cfpLoadingBarProvider.includeSpinner = false; // decorate the tooltip getter - var tooltipFactory = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; + 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 = tooltipFactory.apply(this, arguments); + 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. @@ -53,6 +53,7 @@ export function quayConfig( // Configure analytics. if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { + let $analyticsProvider: any = $injector.get('$analyticsProvider'); $analyticsProvider.virtualPageviews(true); } From 537c07ad4dfb005c66f13caa918f8c83ce09b872 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 1 Feb 2017 00:29:56 -0800 Subject: [PATCH 19/19] fixes based on requested changes --- karma.conf.js | 4 +- .../js/constants/injected-values.constant.ts | 2 +- static/js/constants/name-patterns.constant.ts | 2 +- static/js/constants/pages.constant.ts | 19 +++--- .../inject/inject.decorator.spec.ts | 2 +- .../js/decorators/inject/inject.decorator.ts | 2 +- static/js/quay-pages.module.ts | 2 +- static/js/quay.config.ts | 6 +- static/js/quay.module.ts | 6 +- static/js/quay.routes.ts | 3 +- static/js/quay.run.ts | 60 ++++++++++++------- 11 files changed, 60 insertions(+), 48 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 7ace21f46..98114ba2f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -33,9 +33,7 @@ module.exports = function(config) { // Tests utils 'static/test/**/*.js', ], - exclude: [ - 'static/js/build/bundle.js', - ], + exclude: [], preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], diff --git a/static/js/constants/injected-values.constant.ts b/static/js/constants/injected-values.constant.ts index dcfbcbd32..f7bd2f9a4 100644 --- a/static/js/constants/injected-values.constant.ts +++ b/static/js/constants/injected-values.constant.ts @@ -13,4 +13,4 @@ export const INJECTED_ENDPOINTS: any = (window).__endpoints; /** * Features information. */ -export const INJECTED_FEATURES: any = (window).__features; \ No newline at end of file +export const INJECTED_FEATURES: any = (window).__features; diff --git a/static/js/constants/name-patterns.constant.ts b/static/js/constants/name-patterns.constant.ts index a90b027ff..422887c3e 100644 --- a/static/js/constants/name-patterns.constant.ts +++ b/static/js/constants/name-patterns.constant.ts @@ -5,4 +5,4 @@ export default { TEAM_PATTERN: '^[a-z][a-z0-9]+$', ROBOT_PATTERN: '^[a-z][a-z0-9_]{1,254}$', USERNAME_PATTERN: '^(?=.{2,255}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$', -}; \ No newline at end of file +}; diff --git a/static/js/constants/pages.constant.ts b/static/js/constants/pages.constant.ts index 57b6eb7fd..6d0452b8c 100644 --- a/static/js/constants/pages.constant.ts +++ b/static/js/constants/pages.constant.ts @@ -3,24 +3,23 @@ * TODO: Convert to class/Angular service */ export default { - '_pages': {}, + _pages: {}, /** * Create a page. * @param pageName The name of the page. * @param templateName The file name of the template. - * @param opt_controller Controller for the page. - * @param opt_flags Additional flags passed to route provider. - * @param opt_profiles Available profiles. + * @param controller Controller for the page. + * @param flags Additional flags passed to route provider. + * @param profiles Available profiles. */ - 'create': function (pageName, templateName, opt_controller, opt_flags, opt_profiles) { - var profiles = opt_profiles || ['old-layout', 'layout']; + create: function(pageName: string, templateName: string, controller?: Object, flags = {}, profiles = ['old-layout', 'layout']) { for (var i = 0; i < profiles.length; ++i) { this._pages[profiles[i] + ':' + pageName] = { 'name': pageName, - 'controller': opt_controller, + 'controller': controller, 'templateName': templateName, - 'flags': opt_flags || {} + 'flags': flags }; } }, @@ -30,7 +29,7 @@ export default { * @param pageName The name of the page. * @param profiles Available profiles to search. */ - 'get': function (pageName, profiles) { + get: function(pageName: string, profiles: any[]) { for (var i = 0; i < profiles.length; ++i) { var current = profiles[i]; var key = current.id + ':' + pageName; @@ -42,4 +41,4 @@ export default { return null; } -}; \ No newline at end of file +}; diff --git a/static/js/decorators/inject/inject.decorator.spec.ts b/static/js/decorators/inject/inject.decorator.spec.ts index 2f0a9a17b..f3f015e20 100644 --- a/static/js/decorators/inject/inject.decorator.spec.ts +++ b/static/js/decorators/inject/inject.decorator.spec.ts @@ -14,4 +14,4 @@ describe("Decorator: Inject", () => { class ValidService { constructor(@Inject('$scope') private $scope: any) {} -} \ No newline at end of file +} diff --git a/static/js/decorators/inject/inject.decorator.ts b/static/js/decorators/inject/inject.decorator.ts index 2c647a4c7..dadc20a58 100644 --- a/static/js/decorators/inject/inject.decorator.ts +++ b/static/js/decorators/inject/inject.decorator.ts @@ -5,7 +5,7 @@ */ export function Inject(value: string) { return (target: any, propertyKey: string | symbol, parameterIndex: number): void => { - target.$inject = target.$inject || []; + target.$inject = target.$inject = []; target.$inject[parameterIndex] = value; } } diff --git a/static/js/quay-pages.module.ts b/static/js/quay-pages.module.ts index 072307c58..871cb78f0 100644 --- a/static/js/quay-pages.module.ts +++ b/static/js/quay-pages.module.ts @@ -9,4 +9,4 @@ export default angular .directive('rpHeader', rpHeaderDirective) .directive('rpSidebar', rpSidebarDirective) .directive('rpBody', rpBodyDirective) - .name; \ No newline at end of file + .name; diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts index fc7fc7293..af2a36af9 100644 --- a/static/js/quay.config.ts +++ b/static/js/quay.config.ts @@ -29,10 +29,8 @@ export function quayConfig( 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.attr('bs-tooltip') == null) { + if (element !== undefined && element.attr('bs-tooltip') == null) { return existing.apply(this, arguments); - } else { - return null; } }; } @@ -66,4 +64,4 @@ export function quayConfig( }; }); } -} \ No newline at end of file +} diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index b5d17353b..c149f1ba3 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -28,7 +28,9 @@ var quayDependencies: string[] = [ 'react' ]; -if (INJECTED_CONFIG && (INJECTED_CONFIG.MIXPANEL_KEY || INJECTED_CONFIG.MUNCHKIN_KEY || INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY)) { +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) { @@ -54,4 +56,4 @@ export default angular .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS) .service('ViewArray', ViewArrayImpl) .run(quayRun) - .name; \ No newline at end of file + .name; diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts index e458e5715..628c77dbd 100644 --- a/static/js/quay.routes.ts +++ b/static/js/quay.routes.ts @@ -1,6 +1,7 @@ import { RouteBuilderImpl } from './services/route-builder/route-builder.service.impl'; import { RouteBuilder } from './services/route-builder/route-builder.service'; import pages from './constants/pages.constant'; +import * as ng from '@types/angular'; routeConfig.$inject = [ @@ -131,4 +132,4 @@ export function routeConfig( .route('/:catch/:all', 'error-view') .route('/:catch/:all/:things', 'error-view') .route('/:catch/:all/:things/:here', 'error-view'); -} \ No newline at end of file +} diff --git a/static/js/quay.run.ts b/static/js/quay.run.ts index ac28801b6..9ebcfd262 100644 --- a/static/js/quay.run.ts +++ b/static/js/quay.run.ts @@ -1,4 +1,5 @@ import * as $ from 'jquery'; +import * as ng from '@types/angular'; quayRun.$inject = [ @@ -18,42 +19,45 @@ quayRun.$inject = [ ]; export default function quayRun( - $location, - $rootScope, - Restangular, - UserService, - PlanService, - $http, - $timeout, - CookieService, - Features, - $anchorScroll, - UtilService, - MetaService, - INJECTED_CONFIG) { + $location: ng.ILocationService, + $rootScope: QuayRunScope, + Restangular: any, + UserService: any, + PlanService: any, + $http: ng.IHttpService, + $timeout: ng.ITimeoutService, + CookieService: any, + Features: any, + $anchorScroll: ng.IAnchorScrollService, + UtilService: any, + MetaService: any, + INJECTED_CONFIG: any) { var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; // Handle session security. - Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], {'_csrf_token': (window).__token || ''}); + Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], + {'_csrf_token': (window).__token || ''}); // Handle session expiration. Restangular.setErrorInterceptor(function(response) { - if (response.status == 503) { + if (response !== undefined && response.status == 503) { ($('#cannotContactService')).modal({}); return false; } - if (response.status == 500) { + if (response !== undefined && response.status == 500) { window.location.href = '/500'; return false; } - if (!response.data) { + if (response !== undefined && !response.data) { return true; } var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; - if (response.status == 401 && invalid_token && response.data['session_required'] !== false) { + if (response !== undefined && response.status == 401 && + invalid_token && + response.data['session_required'] !== false) { ($('#sessionexpiredModal')).modal({}); return false; } @@ -73,9 +77,10 @@ export default function quayRun( return; } - $rootScope.$watch('description', function(description) { + $rootScope.$watch('description', function(description: string) { if (!description) { - description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.'; + 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 @@ -87,9 +92,7 @@ export default function quayRun( // Listen for scope changes and update the title and description accordingly. $rootScope.$watch(function() { var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; - if ($rootScope.title != title) { - $rootScope.title = title; - } + $rootScope.title = title; var description = MetaService.getDescription($rootScope.currentPage) || ''; if ($rootScope.description != description) { @@ -126,4 +129,15 @@ export default function quayRun( } return $http.pendingRequests.length > 0; }; +} + + +interface QuayRunScope extends ng.IRootScopeService { + currentPage: any; + current: any; + title: any; + description: string, + pageClass: any; + newLayout: any; + fixFooter: any; } \ No newline at end of file