diff --git a/external_libraries.py b/external_libraries.py index f2f9c3832..ef1a9d03f 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/package.json b/package.json index 7a471e930..e26efe744 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/react": "0.14.39", "@types/react-dom": "0.14.17", "angular-mocks": "^1.5.3", + "angular-ts-decorators": "0.0.19", "css-loader": "0.25.0", "jasmine-core": "^2.5.2", "jasmine-ts": "0.0.3", diff --git a/static/directives/manage-trigger-githost.html b/static/directives/manage-trigger-githost.html index 4912b5c2f..4645a2f2f 100644 --- a/static/directives/manage-trigger-githost.html +++ b/static/directives/manage-trigger-githost.html @@ -184,10 +184,10 @@
Examples: heads/master, tags/tagname, heads/.+
-
+ diff --git a/static/directives/regex-match-view.html b/static/directives/regex-match-view.html deleted file mode 100644 index da9316838..000000000 --- a/static/directives/regex-match-view.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- Invalid Regular Expression! -
-
- - - - - - - - - -
Matching: -
    -
  • - {{ item.title }} -
  • -
-
Not Matching: -
    -
  • - {{ item.title }} -
  • -
-
-
-
\ No newline at end of file diff --git a/static/js/constants/name-patterns.constant.ts b/static/js/constants/name-patterns.constant.ts index 422887c3e..942028ee1 100644 --- a/static/js/constants/name-patterns.constant.ts +++ b/static/js/constants/name-patterns.constant.ts @@ -1,7 +1,7 @@ /** * Regex patterns to for validating account names. */ -export default { +export const NAME_PATTERNS: any = { 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]+)*)$', diff --git a/static/js/constants/pages.constant.ts b/static/js/constants/pages.constant.ts deleted file mode 100644 index 6d0452b8c..000000000 --- a/static/js/constants/pages.constant.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * 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 controller Controller for the page. - * @param flags Additional flags passed to route provider. - * @param profiles Available profiles. - */ - 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': controller, - 'templateName': templateName, - 'flags': flags - }; - } - }, - - /** - * Retrieve a registered page. - * @param pageName The name of the page. - * @param profiles Available profiles to search. - */ - get: function(pageName: string, profiles: any[]) { - 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; - } -}; diff --git a/static/js/directives/ui/regex-match-view.js b/static/js/directives/ui/regex-match-view.js deleted file mode 100644 index 6bd2380a5..000000000 --- a/static/js/directives/ui/regex-match-view.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * An element which displays the matches and non-matches for a regular expression against a set of - * items. - */ -angular.module('quay').directive('regexMatchView', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/regex-match-view.html', - replace: false, - transclude: true, - restrict: 'C', - scope: { - 'regex': '=regex', - 'items': '=items' - }, - controller: function($scope, $element) { - $scope.filterMatches = function(regexstr, items, shouldMatch) { - regexstr = regexstr || '.+'; - - try { - var regex = new RegExp(regexstr); - } catch (ex) { - return null; - } - - return items.filter(function(item) { - var value = item['value']; - var m = value.match(regex); - var matches = !!(m && m[0].length == value.length); - return matches == shouldMatch; - }); - }; - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts new file mode 100644 index 000000000..86e18b99c --- /dev/null +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts @@ -0,0 +1,14 @@ +import { RegexMatchViewComponent } from './regex-match-view.component'; + + +describe("RegexMatchViewComponent", () => { + var component: RegexMatchViewComponent; + + beforeEach(() => { + component = new RegexMatchViewComponent(); + }); + + describe("filterMatches", () => { + + }); +}); diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts new file mode 100644 index 000000000..2c987906c --- /dev/null +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts @@ -0,0 +1,67 @@ +import { Input, Component } from 'angular-ts-decorators'; + + +/** + * An element which displays the matches and non-matches for a regular expression against a set of + * items. + */ +@Component({ + selector: 'regexMatchView', + template: ` +
+
+ Invalid Regular Expression! +
+
+ + + + + + + + + +
Matching: +
    +
  • + {{ item.title }} +
  • +
+
Not Matching: +
    +
  • + {{ item.title }} +
  • +
+
+
+
+ ` +}) +export class RegexMatchViewComponent implements ng.IComponentController { + + @Input('=') private regex: string; + @Input('=') private items: any[]; + + constructor() { + + } + + public filterMatches(regexstr: string, items: any[], shouldMatch: boolean): any[] { + regexstr = regexstr || '.+'; + + try { + var regex = new RegExp(regexstr); + } catch (ex) { + return null; + } + + return items.filter(function(item) { + var value = item['value']; + var m = value.match(regex); + var matches: boolean = !!(m && m[0].length == value.length); + return matches == shouldMatch; + }); + } +} diff --git a/static/js/quay-pages.module.ts b/static/js/quay-pages.module.ts index 871cb78f0..23ecc26a0 100644 --- a/static/js/quay-pages.module.ts +++ b/static/js/quay-pages.module.ts @@ -1,12 +1,28 @@ import * as angular from 'angular'; import { rpHeaderDirective, rpBodyDirective, rpSidebarDirective } from './directives/components/pages/repo-page/main'; -import pages from './constants/pages.constant'; +import { PageServiceImpl } from './services/page/page.service.impl'; +import { NgModule } from 'angular-ts-decorators'; -export default angular - .module('quayPages', []) - .constant('pages', pages) +/** + * Module containing registered application page/view components. + */ +@NgModule({ + imports: [], + declarations: [], + providers: [ + PageServiceImpl, + ] +}) +export class quayPages { + +} + + +// TODO: Move component registration to @NgModule and remove this. +angular + .module(quayPages.name) + .constant('pages', new PageServiceImpl()) .directive('rpHeader', rpHeaderDirective) .directive('rpSidebar', rpSidebarDirective) - .directive('rpBody', rpBodyDirective) - .name; + .directive('rpBody', rpBodyDirective); diff --git a/static/js/quay-routes.module.ts b/static/js/quay-routes.module.ts new file mode 100644 index 000000000..13715a264 --- /dev/null +++ b/static/js/quay-routes.module.ts @@ -0,0 +1,146 @@ +import { RouteBuilderImpl } from './services/route-builder/route-builder.service.impl'; +import { RouteBuilder } from './services/route-builder/route-builder.service'; +import { PageService } from './services/page/page.service'; +import * as ng from '@types/angular'; +import { NgModule } from 'angular-ts-decorators'; +import { INJECTED_FEATURES } from './constants/injected-values.constant'; +import { quayPages } from './quay-pages.module'; + + +/** + * Module containing client-side routing configuration. + */ +@NgModule({ + imports: [ + quayPages, + 'ngRoute', + ], + declarations: [], + providers: [], +}) +export class QuayRoutes { + + public config($routeProvider: ng.route.IRouteProvider, + $locationProvider: ng.ILocationProvider, + pages: PageService): void { + $locationProvider.html5Mode(true); + + // WARNING WARNING WARNING + // If you add a route here, you must add a corresponding route in thr endpoints/web.py + // index rule to make sure that deep links directly deep into the app continue to work. + // WARNING WARNING WARNING + + var routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pages.$get()); + + if (INJECTED_FEATURES.SUPER_USERS) { + // QE Management + routeBuilder.route('/superuser/', 'superuser') + // QE Setup + .route('/setup/', 'setup'); + } + + routeBuilder + // Repository View + .route('/repository/:namespace/:name', 'repo-view') + .route('/repository/:namespace/:name/tag/:tag', 'repo-view') + + // Image View + .route('/repository/:namespace/:name/image/:image', 'image-view') + + // Repo Build View + .route('/repository/:namespace/:name/build/:buildid', 'build-view') + + // Repo Trigger View + .route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup') + + // Create repository notification + .route('/repository/:namespace/:name/create-notification', 'create-repository-notification') + + // Repo List + .route('/repository/', 'repo-list') + + // Organizations + .route('/organizations/', 'organizations') + + // New Organization + .route('/organizations/new/', 'new-organization') + + // View Organization + .route('/organization/:orgname', 'org-view') + + // View Organization Team + .route('/organization/:orgname/teams/:teamname', 'team-view') + + // Organization View Application + .route('/organization/:orgname/application/:clientid', 'manage-application') + + // View Organization Billing + .route('/organization/:orgname/billing', 'billing') + + // View Organization Billing Invoices + .route('/organization/:orgname/billing/invoices', 'invoices') + + // View User + .route('/user/:username', 'user-view') + + // View User Billing + .route('/user/:username/billing', 'billing') + + // View User Billing Invoices + .route('/user/:username/billing/invoices', 'invoices') + + // Sign In + .route('/signin/', 'signin') + + // New Repository + .route('/new/', 'new-repo') + + // Plans + .route('/plans/', 'plans') + + // Tutorial + .route('/tutorial/', 'tutorial') + + // Contact + .route('/contact/', 'contact') + + // About + .route('/about/', 'about') + + // Security + .route('/security/', 'security') + + // TOS + .route('/tos', 'tos') + + // Privacy + .route('/privacy', 'privacy') + + // Change username + .route('/updateuser', 'update-user') + + // Landing Page + .route('/', 'landing') + + // Tour + .route('/tour/', 'tour') + .route('/tour/features', 'tour') + .route('/tour/organizations', 'tour') + .route('/tour/enterprise', 'tour') + + // Confirm Invite + .route('/confirminvite', 'confirm-invite') + + // 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'); + } +} diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts deleted file mode 100644 index af2a36af9..000000000 --- a/static/js/quay.config.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as Raven from 'raven-js'; - - -quayConfig.$inject = [ - '$provide', - '$injector', - 'INJECTED_CONFIG', - 'cfpLoadingBarProvider', - '$tooltipProvider', - '$compileProvider', - 'RestangularProvider', -]; - -export function quayConfig( - $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: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; - $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { - if ('ontouchstart' in $window) { - var existing: any = tooltipFactory.apply(this, arguments); - return function(element) { - // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things - // (such as the datepicker), so we need to be specific when canceling it. - if (element !== undefined && element.attr('bs-tooltip') == null) { - return existing.apply(this, arguments); - } - }; - } - - return tooltipFactory.apply(this, arguments); - }; - - if (!INJECTED_CONFIG['DEBUG']) { - $compileProvider.debugInfoEnabled(false); - } - - // Configure compile provider to add additional URL prefixes to the sanitization list. We use - // these on the Contact page. - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); - - // Configure the API provider. - RestangularProvider.setBaseUrl('/api/v1/'); - - // Configure analytics. - if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { - let $analyticsProvider: any = $injector.get('$analyticsProvider'); - $analyticsProvider.virtualPageviews(true); - } - - // Configure sentry. - if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { - $provide.decorator("$exceptionHandler", function($delegate) { - return function(ex, cause) { - $delegate(ex, cause); - Raven.captureException(ex, {extra: {cause: cause}}); - }; - }); - } -} diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index c149f1ba3..6e37c162f 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -1,16 +1,15 @@ -import * as angular from 'angular'; -import { quayConfig } from './quay.config'; -import quayPages from './quay-pages.module'; -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 { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from './constants/injected-values.constant'; +import * as angular from "angular"; +import * as Raven from "raven-js"; +import { ViewArrayImpl } from "./services/view-array/view-array.impl"; +import { NAME_PATTERNS } from "./constants/name-patterns.constant"; +import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; +import { RegexMatchViewComponent } from "./directives/ui/regex-match-view/regex-match-view.component"; +import { NgModule } from "angular-ts-decorators"; +import { QuayRoutes } from "./quay-routes.module"; -var quayDependencies: string[] = [ - quayPages, - 'ngRoute', +var quayDependencies: any[] = [ + QuayRoutes, 'chieffancypants.loadingBar', 'cfp.hotkeys', 'angular-tour', @@ -46,14 +45,198 @@ if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { quayDependencies.push('vcRecaptcha'); } -export default angular - .module('quay', quayDependencies) - .config(quayConfig) - .config(routeConfig) + +/** + * Main application module. + */ +@NgModule({ + imports: quayDependencies, + declarations: [ + RegexMatchViewComponent, + ], + providers: [ + ViewArrayImpl, + ], +}) +export class quay { + + public config($provide: ng.auto.IProvideService, + $injector: ng.auto.IInjectorService, + INJECTED_CONFIG: any, + cfpLoadingBarProvider: any, + $tooltipProvider: any, + $compileProvider: ng.ICompileProvider, + RestangularProvider: any): void { + cfpLoadingBarProvider.includeSpinner = false; + + // decorate the tooltip getter + var tooltipFactory: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; + $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { + if ('ontouchstart' in $window) { + var existing: any = tooltipFactory.apply(this, arguments); + return function(element) { + // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things + // (such as the datepicker), so we need to be specific when canceling it. + if (element !== undefined && element.attr('bs-tooltip') == null) { + return existing.apply(this, arguments); + } + }; + } + + return tooltipFactory.apply(this, arguments); + }; + + if (!INJECTED_CONFIG['DEBUG']) { + $compileProvider.debugInfoEnabled(false); + } + + // Configure compile provider to add additional URL prefixes to the sanitization list. We use + // these on the Contact page. + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); + + // Configure the API provider. + RestangularProvider.setBaseUrl('/api/v1/'); + + // Configure analytics. + if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { + let $analyticsProvider: any = $injector.get('$analyticsProvider'); + $analyticsProvider.virtualPageviews(true); + } + + // Configure sentry. + if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { + $provide.decorator("$exceptionHandler", function($delegate) { + return function(ex, cause) { + $delegate(ex, cause); + Raven.captureException(ex, {extra: {cause: cause}}); + }; + }); + } + } + + public run($rootScope: QuayRunScope, + Restangular: any, + PlanService: any, + $http: ng.IHttpService, + CookieService: any, + Features: any, + $anchorScroll: ng.IAnchorScrollService, + MetaService: any, + INJECTED_CONFIG: any): void { + var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; + + // Handle session security. + Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], + {'_csrf_token': (window).__token || ''}); + + // Handle session expiration. + Restangular.setErrorInterceptor(function(response) { + if (response !== undefined && response.status == 503) { + ($('#cannotContactService')).modal({}); + return false; + } + + if (response !== undefined && response.status == 500) { + window.location.href = '/500'; + return false; + } + + if (response !== undefined && !response.data) { + return true; + } + + var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; + if (response !== undefined && response.status == 401 && + invalid_token && + response.data['session_required'] !== false) { + ($('#sessionexpiredModal')).modal({}); + return false; + } + + return true; + }); + + // Check if we need to redirect based on a previously chosen plan. + var result = PlanService.handleNotedPlan(); + + // Check to see if we need to show a redirection page. + var redirectUrl = CookieService.get('quay.redirectAfterLoad'); + CookieService.clear('quay.redirectAfterLoad'); + + if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { + (window).location = redirectUrl; + return; + } + + $rootScope.$watch('description', function(description: string) { + if (!description) { + description = `Hosted private docker repositories. Includes full user management and history. + Free for public repositories.`; + } + + // Note: We set the content of the description tag manually here rather than using Angular binding + // because we need the tag to have a default description that is not of the form "{{ description }}", + // we read by tools that do not properly invoke the Angular code. + $('#descriptionTag').attr('content', description); + }); + + // Listen for scope changes and update the title and description accordingly. + $rootScope.$watch(function() { + var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; + $rootScope.title = title; + + var description = MetaService.getDescription($rootScope.currentPage) || ''; + if ($rootScope.description != description) { + $rootScope.description = description; + } + }); + + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + $rootScope.current = current.$$route; + $rootScope.currentPage = current; + $rootScope.pageClass = ''; + + if (!current.$$route) { return; } + + var pageClass = current.$$route.pageClass || ''; + if (typeof pageClass != 'string') { + pageClass = pageClass(Features); + } + + $rootScope.pageClass = pageClass; + $rootScope.newLayout = !!current.$$route.newLayout; + $rootScope.fixFooter = !!current.$$route.fixFooter; + + $anchorScroll(); + }); + + var initallyChecked: boolean = false; + (window).__isLoading = function() { + if (!initallyChecked) { + initallyChecked = true; + return true; + } + return $http.pendingRequests.length > 0; + }; + } +} + + +interface QuayRunScope extends ng.IRootScopeService { + currentPage: any; + current: any; + title: any; + description: string, + pageClass: any; + newLayout: any; + fixFooter: any; +} + + +// TODO: Move component registration to @NgModule and remove this. +angular + .module(quay.name) .constant('NAME_PATTERNS', NAME_PATTERNS) .constant('INJECTED_CONFIG', INJECTED_CONFIG) .constant('INJECTED_FEATURES', INJECTED_FEATURES) - .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS) - .service('ViewArray', ViewArrayImpl) - .run(quayRun) - .name; + .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS); diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts deleted file mode 100644 index 1714c51ba..000000000 --- a/static/js/quay.routes.ts +++ /dev/null @@ -1,138 +0,0 @@ -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 = [ - 'pages', - '$routeProvider', - '$locationProvider', - 'INJECTED_FEATURES', -]; - -export function routeConfig( - pages: any, - $routeProvider: ng.route.IRouteProvider, - $locationProvider: ng.ILocationProvider, - INJECTED_FEATURES) { - $locationProvider.html5Mode(true); - - // WARNING WARNING WARNING - // If you add a route here, you must add a corresponding route in thr endpoints/web.py - // index rule to make sure that deep links directly deep into the app continue to work. - // WARNING WARNING WARNING - - var routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pages); - - if (INJECTED_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') - - // Repo Trigger View - .route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup') - - // Create repository notification - .route('/repository/:namespace/:name/create-notification', 'create-repository-notification') - - // Repo List - .route('/repository/', 'repo-list') - - // Organizations - .route('/organizations/', 'organizations') - - // New Organization - .route('/organizations/new/', 'new-organization') - - // View Organization - .route('/organization/:orgname', 'org-view') - - // View Organization Team - .route('/organization/:orgname/teams/:teamname', 'team-view') - - // Organization View Application - .route('/organization/:orgname/application/:clientid', 'manage-application') - - // View Organization Billing - .route('/organization/:orgname/billing', 'billing') - - // View Organization Billing Invoices - .route('/organization/:orgname/billing/invoices', 'invoices') - - // View User - .route('/user/:username', 'user-view') - - // View User Billing - .route('/user/:username/billing', 'billing') - - // View User Billing Invoices - .route('/user/:username/billing/invoices', 'invoices') - - // Sign In - .route('/signin/', 'signin') - - // New Repository - .route('/new/', 'new-repo') - - // Plans - .route('/plans/', 'plans') - - // Tutorial - .route('/tutorial/', 'tutorial') - - // Contact - .route('/contact/', 'contact') - - // About - .route('/about/', 'about') - - // Security - .route('/security/', 'security') - - // TOS - .route('/tos', 'tos') - - // Privacy - .route('/privacy', 'privacy') - - // Change username - .route('/updateuser', 'update-user') - - // Landing Page - .route('/', 'landing') - - // Tour - .route('/tour/', 'tour') - .route('/tour/features', 'tour') - .route('/tour/organizations', 'tour') - .route('/tour/enterprise', 'tour') - - // Confirm Invite - .route('/confirminvite', 'confirm-invite') - - // 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'); -} diff --git a/static/js/quay.run.ts b/static/js/quay.run.ts deleted file mode 100644 index 9ebcfd262..000000000 --- a/static/js/quay.run.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as $ from 'jquery'; -import * as ng from '@types/angular'; - - -quayRun.$inject = [ - '$location', - '$rootScope', - 'Restangular', - 'UserService', - 'PlanService', - '$http', - '$timeout', - 'CookieService', - 'Features', - '$anchorScroll', - 'UtilService', - 'MetaService', - 'INJECTED_CONFIG', -]; - -export default function quayRun( - $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 || ''}); - - // Handle session expiration. - Restangular.setErrorInterceptor(function(response) { - if (response !== undefined && response.status == 503) { - ($('#cannotContactService')).modal({}); - return false; - } - - if (response !== undefined && response.status == 500) { - window.location.href = '/500'; - return false; - } - - if (response !== undefined && !response.data) { - return true; - } - - var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; - if (response !== undefined && response.status == 401 && - invalid_token && - response.data['session_required'] !== false) { - ($('#sessionexpiredModal')).modal({}); - return false; - } - - return true; - }); - - // Check if we need to redirect based on a previously chosen plan. - var result = PlanService.handleNotedPlan(); - - // Check to see if we need to show a redirection page. - var redirectUrl = CookieService.get('quay.redirectAfterLoad'); - CookieService.clear('quay.redirectAfterLoad'); - - if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { - (window).location = redirectUrl; - return; - } - - $rootScope.$watch('description', function(description: string) { - if (!description) { - description = `Hosted private docker repositories. Includes full user management and history. - Free for public repositories.`; - } - - // Note: We set the content of the description tag manually here rather than using Angular binding - // because we need the tag to have a default description that is not of the form "{{ description }}", - // we read by tools that do not properly invoke the Angular code. - $('#descriptionTag').attr('content', description); - }); - - // Listen for scope changes and update the title and description accordingly. - $rootScope.$watch(function() { - var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; - $rootScope.title = title; - - var description = MetaService.getDescription($rootScope.currentPage) || ''; - if ($rootScope.description != description) { - $rootScope.description = description; - } - }); - - $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { - $rootScope.current = current.$$route; - $rootScope.currentPage = current; - - $rootScope.pageClass = ''; - - if (!current.$$route) { return; } - - var pageClass = current.$$route.pageClass || ''; - if (typeof pageClass != 'string') { - pageClass = pageClass(Features); - } - - - $rootScope.pageClass = pageClass; - $rootScope.newLayout = !!current.$$route.newLayout; - $rootScope.fixFooter = !!current.$$route.fixFooter; - - $anchorScroll(); - }); - - var initallyChecked = false; - (window).__isLoading = function() { - if (!initallyChecked) { - initallyChecked = true; - return true; - } - return $http.pendingRequests.length > 0; - }; -} - - -interface QuayRunScope extends ng.IRootScopeService { - currentPage: any; - current: any; - title: any; - description: string, - pageClass: any; - newLayout: any; - fixFooter: any; -} \ No newline at end of file diff --git a/static/js/services/page/page.service.impl.spec.ts b/static/js/services/page/page.service.impl.spec.ts new file mode 100644 index 000000000..61f9e51f1 --- /dev/null +++ b/static/js/services/page/page.service.impl.spec.ts @@ -0,0 +1,22 @@ +import { PageServiceImpl } from './page.service.impl'; + + +describe("Service: PageServiceImpl", () => { + var pageServiceImpl: PageServiceImpl; + + beforeEach(() => { + pageServiceImpl = new PageServiceImpl(); + }); + + describe("create", () => { + + }); + + describe("get", () => { + + }); + + describe("$get", () => { + + }); +}); \ No newline at end of file diff --git a/static/js/services/page/page.service.impl.ts b/static/js/services/page/page.service.impl.ts new file mode 100644 index 000000000..d52818899 --- /dev/null +++ b/static/js/services/page/page.service.impl.ts @@ -0,0 +1,45 @@ +import { Injectable } from 'angular-ts-decorators'; +import { PageService } from './page.service'; + + +@Injectable(PageService.name) +export class PageServiceImpl implements ng.IServiceProvider { + + private pages: any = {}; + + constructor() { + + } + + public create(pageName: string, + templateName: string, + controller?: any, + flags: any = {}, + profiles: string[] = ['old-layout', 'layout']): void { + for (var i = 0; i < profiles.length; ++i) { + this.pages[profiles[i] + ':' + pageName] = { + 'name': pageName, + 'controller': controller, + 'templateName': templateName, + 'flags': flags + }; + } + } + + public get(pageName: string, profiles: any[]): any[] | null { + 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; + } + + public $get(): PageService { + return this; + } +} diff --git a/static/js/services/page/page.service.ts b/static/js/services/page/page.service.ts new file mode 100644 index 000000000..829959d2a --- /dev/null +++ b/static/js/services/page/page.service.ts @@ -0,0 +1,32 @@ +/** + * Manages the creation and retrieval of pages (route + controller) + */ +export abstract class PageService implements ng.IServiceProvider { + + /** + * Create a page. + * @param pageName The name of the page. + * @param templateName The file name of the template. + * @param controller Controller for the page. + * @param flags Additional flags passed to route provider. + * @param profiles Available profiles. + */ + public abstract create(pageName: string, + templateName: string, + controller?: any, + flags?: any, + profiles?: string[]): void; + + /** + * Retrieve a registered page. + * @param pageName The name of the page. + * @param profiles Available profiles to search. + */ + public abstract get(pageName: string, profiles: any[]): any[] | null; + + /** + * Provide the service instance. + * @return pageService The singleton service instance. + */ + public abstract $get(): PageService; +} diff --git a/static/js/services/view-array/view-array.impl.ts b/static/js/services/view-array/view-array.impl.ts index 8cc3f75c4..939d50e5c 100644 --- a/static/js/services/view-array/view-array.impl.ts +++ b/static/js/services/view-array/view-array.impl.ts @@ -1,7 +1,9 @@ import { ViewArray } from './view-array'; import { Inject } from '../../decorators/inject/inject.decorator'; +import { Injectable } from 'angular-ts-decorators'; +@Injectable(ViewArray.name) export class ViewArrayImpl implements ViewArray { public entries: any[];