Merge pull request #2292 from coreos-inc/frontend-typescript
Upgrading Front-end Client to TypeScript
This commit is contained in:
commit
7c904f2e21
38 changed files with 1021 additions and 759 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -8,7 +8,7 @@ dest
|
||||||
node_modules
|
node_modules
|
||||||
static/ldn
|
static/ldn
|
||||||
static/fonts
|
static/fonts
|
||||||
static/js/build
|
static/build
|
||||||
stack_local
|
stack_local
|
||||||
test/data/registry/
|
test/data/registry/
|
||||||
typings
|
typings
|
||||||
|
|
|
@ -161,7 +161,8 @@ def render_page_template(name, route_data=None, **kwargs):
|
||||||
library_styles = list_files('lib', 'css')
|
library_styles = list_files('lib', 'css')
|
||||||
main_styles = list_files('css', 'css')
|
main_styles = list_files('css', 'css')
|
||||||
library_scripts = list_files('lib', 'js')
|
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]
|
file_lists = [library_styles, main_styles, library_scripts, main_scripts]
|
||||||
for file_list in file_lists:
|
for file_list in file_lists:
|
||||||
|
|
|
@ -25,8 +25,13 @@ module.exports = function(grunt) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
src: ['../static/lib/**/*.js', '../static/js/**/*.js', '../static/dist/template-cache.js',
|
src: [
|
||||||
'!../static/js/**/*.spec.js'],
|
'../static/lib/**/*.js',
|
||||||
|
'../static/build/*.js',
|
||||||
|
'../static/js/**/*.js',
|
||||||
|
'../static/dist/template-cache.js',
|
||||||
|
'!../static/js/**/*.spec.js'
|
||||||
|
],
|
||||||
dest: '../static/dist/<%= pkg.name %>.js'
|
dest: '../static/dist/<%= pkg.name %>.js'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
module.exports = function (config) {
|
var webpackConfig = require('./webpack.config');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = function(config) {
|
||||||
config.set({
|
config.set({
|
||||||
basePath: '',
|
basePath: '',
|
||||||
frameworks: ['jasmine'],
|
frameworks: ['jasmine'],
|
||||||
|
@ -25,23 +28,22 @@ module.exports = function (config) {
|
||||||
'static/lib/**/*.js',
|
'static/lib/**/*.js',
|
||||||
|
|
||||||
// Application resources
|
// Application resources
|
||||||
'static/js/**/*.js',
|
'static/js/**/*.spec.ts*',
|
||||||
|
|
||||||
// Tests
|
// Tests utils
|
||||||
'static/test/**/*.js',
|
'static/test/**/*.js',
|
||||||
],
|
],
|
||||||
exclude: [
|
exclude: [],
|
||||||
'static/js/build/bundle.js',
|
|
||||||
],
|
|
||||||
preprocessors: {
|
preprocessors: {
|
||||||
'static/lib/ngReact/react.ngReact.min.js': ['webpack'],
|
'static/lib/ngReact/react.ngReact.min.js': ['webpack'],
|
||||||
'static/lib/angular-moment.min.js': ['webpack'],
|
'static/lib/angular-moment.min.js': ['webpack'],
|
||||||
|
'static/js/**/*.ts*': ['webpack'],
|
||||||
},
|
},
|
||||||
webpack: {},
|
webpack: webpackConfig,
|
||||||
webpackMiddleware: {
|
webpackMiddleware: {
|
||||||
stats: 'errors-only'
|
stats: 'errors-only'
|
||||||
},
|
},
|
||||||
reporters: ['dots', 'coverage'],
|
reporters: ['dots', 'coverage', 'karma-typescript'],
|
||||||
coverageReporter: {
|
coverageReporter: {
|
||||||
dir: 'coverage',
|
dir: 'coverage',
|
||||||
type: 'html'
|
type: 'html'
|
||||||
|
|
15
package.json
15
package.json
|
@ -5,6 +5,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS",
|
"test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS",
|
||||||
|
"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",
|
"build": "./node_modules/.bin/webpack --progress -p -v",
|
||||||
"watch": "./node_modules/.bin/webpack --watch"
|
"watch": "./node_modules/.bin/webpack --watch"
|
||||||
},
|
},
|
||||||
|
@ -14,9 +15,6 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/coreos-inc/quay#readme",
|
"homepage": "https://github.com/coreos-inc/quay#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/angular": "1.5.16",
|
|
||||||
"@types/react": "0.14.39",
|
|
||||||
"@types/react-dom": "0.14.17",
|
|
||||||
"angular": "1.5.3",
|
"angular": "1.5.3",
|
||||||
"angular-animate": "^1.5.3",
|
"angular-animate": "^1.5.3",
|
||||||
"angular-cookies": "^1.5.3",
|
"angular-cookies": "^1.5.3",
|
||||||
|
@ -36,8 +34,16 @@
|
||||||
"underscore": "^1.5.2"
|
"underscore": "^1.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
||||||
"angular-mocks": "^1.5.3",
|
"angular-mocks": "^1.5.3",
|
||||||
|
"css-loader": "0.25.0",
|
||||||
"jasmine-core": "^2.5.2",
|
"jasmine-core": "^2.5.2",
|
||||||
|
"jasmine-ts": "0.0.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^0.13.22",
|
||||||
"karma-chrome-launcher": "^2.0.0",
|
"karma-chrome-launcher": "^2.0.0",
|
||||||
"karma-coverage": "^0.5.5",
|
"karma-coverage": "^0.5.5",
|
||||||
|
@ -45,12 +51,11 @@
|
||||||
"karma-jasmine": "^0.3.8",
|
"karma-jasmine": "^0.3.8",
|
||||||
"karma-phantomjs-launcher": "^1.0.0",
|
"karma-phantomjs-launcher": "^1.0.0",
|
||||||
"karma-webpack": "^1.8.1",
|
"karma-webpack": "^1.8.1",
|
||||||
"css-loader": "0.25.0",
|
|
||||||
"node-sass": "3.10.1",
|
"node-sass": "3.10.1",
|
||||||
|
"phantomjs-prebuilt": "^2.1.7",
|
||||||
"sass-loader": "4.0.2",
|
"sass-loader": "4.0.2",
|
||||||
"source-map-loader": "0.1.5",
|
"source-map-loader": "0.1.5",
|
||||||
"style-loader": "0.13.1",
|
"style-loader": "0.13.1",
|
||||||
"phantomjs-prebuilt": "^2.1.7",
|
|
||||||
"ts-loader": "0.9.5",
|
"ts-loader": "0.9.5",
|
||||||
"typescript": "2.0.3",
|
"typescript": "2.0.3",
|
||||||
"typings": "1.4.0",
|
"typings": "1.4.0",
|
||||||
|
|
356
static/js/app.js
356
static/js/app.js
|
@ -1,356 +0,0 @@
|
||||||
var TEAM_PATTERN = '^[a-z][a-z0-9]+$';
|
|
||||||
var ROBOT_PATTERN = '^[a-z][a-z0-9_]{1,254}$';
|
|
||||||
var USERNAME_PATTERN = '^(?=.{2,255}$)([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 <meta> 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;
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -1,5 +0,0 @@
|
||||||
// Import Components
|
|
||||||
import {rpDirectives as repoPage} from "./directives/components/pages/repo-page/main";
|
|
||||||
|
|
||||||
// Init for each page
|
|
||||||
repoPage();
|
|
16
static/js/constants/injected-values.constant.ts
Normal file
16
static/js/constants/injected-values.constant.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Configuration data set.
|
||||||
|
*/
|
||||||
|
export const INJECTED_CONFIG: any = (<any>window).__config;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API route information.
|
||||||
|
*/
|
||||||
|
export const INJECTED_ENDPOINTS: any = (<any>window).__endpoints;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Features information.
|
||||||
|
*/
|
||||||
|
export const INJECTED_FEATURES: any = (<any>window).__features;
|
8
static/js/constants/name-patterns.constant.ts
Normal file
8
static/js/constants/name-patterns.constant.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* 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}$',
|
||||||
|
USERNAME_PATTERN: '^(?=.{2,255}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$',
|
||||||
|
};
|
44
static/js/constants/pages.constant.ts
Normal file
44
static/js/constants/pages.constant.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
};
|
17
static/js/decorators/inject/inject.decorator.spec.ts
Normal file
17
static/js/decorators/inject/inject.decorator.spec.ts
Normal file
|
@ -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) {}
|
||||||
|
}
|
11
static/js/decorators/inject/inject.decorator.ts
Normal file
11
static/js/decorators/inject/inject.decorator.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +1,31 @@
|
||||||
import "sass/repo-page/repo-page.scss";
|
import "sass/repo-page/repo-page.scss";
|
||||||
import * as angular from "angular";
|
|
||||||
|
|
||||||
import repoHeader from "./header";
|
import repoHeader from "./header";
|
||||||
import repoSidebar from "./sidebar";
|
import repoSidebar from "./sidebar";
|
||||||
import repoBody from "./body";
|
import repoBody from "./body";
|
||||||
|
|
||||||
export function rpDirectives(){
|
rpHeaderDirective.$inject = [
|
||||||
angular.module('quayPages').directive('rpHeader', function(reactDirective) {
|
'reactDirective',
|
||||||
return reactDirective(repoHeader);
|
];
|
||||||
});
|
|
||||||
|
|
||||||
angular.module('quayPages').directive('rpSidebar', function(reactDirective) {
|
export function rpHeaderDirective(reactDirective) {
|
||||||
return reactDirective(repoSidebar);
|
return reactDirective(repoHeader);
|
||||||
});
|
|
||||||
|
|
||||||
angular.module('quayPages').directive('rpBody', function(reactDirective, ApiService) {
|
|
||||||
return reactDirective(repoBody, undefined, {}, {api: ApiService});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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});
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
// From: http://justinklemm.com/angularjs-filter-ordering-objects-ngrepeat/ under MIT License
|
// 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) {
|
return function(items, field, reverse) {
|
||||||
var filtered = [];
|
var filtered = [];
|
||||||
angular.forEach(items, function(item) {
|
angular.forEach(items, function(item) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ angular.module('quay').directive('buildLogsView', function () {
|
||||||
'buildUpdated': '&buildUpdated',
|
'buildUpdated': '&buildUpdated',
|
||||||
'isSuperUser': '=isSuperUser'
|
'isSuperUser': '=isSuperUser'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray,
|
controller: function($scope, $element, $interval, $sanitize, ansi2html, ViewArray,
|
||||||
AngularPollChannel, ApiService, Restangular, UtilService) {
|
AngularPollChannel, ApiService, Restangular, UtilService) {
|
||||||
|
|
||||||
var repoStatusApiCall = ApiService.getRepoBuildStatus;
|
var repoStatusApiCall = ApiService.getRepoBuildStatus;
|
||||||
|
@ -60,7 +60,7 @@ angular.module('quay').directive('buildLogsView', function () {
|
||||||
var entry = logs[i];
|
var entry = logs[i];
|
||||||
var type = entry['type'] || 'entry';
|
var type = entry['type'] || 'entry';
|
||||||
if (type == 'command' || type == 'phase' || type == 'error') {
|
if (type == 'command' || type == 'phase' || type == 'error') {
|
||||||
entry['logs'] = AngularViewArray.create();
|
entry['logs'] = ViewArray.create();
|
||||||
entry['index'] = $scope.logStartIndex + i;
|
entry['index'] = $scope.logStartIndex + i;
|
||||||
|
|
||||||
$scope.logEntries.push(entry);
|
$scope.logEntries.push(entry);
|
||||||
|
|
|
@ -12,8 +12,8 @@ angular.module('quay').directive('createRobotDialog', function () {
|
||||||
'info': '=info',
|
'info': '=info',
|
||||||
'robotCreated': '&robotCreated'
|
'robotCreated': '&robotCreated'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ApiService, UserService) {
|
controller: function($scope, $element, ApiService, UserService, NAME_PATTERNS) {
|
||||||
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
|
$scope.ROBOT_PATTERN = NAME_PATTERNS.ROBOT_PATTERN;
|
||||||
|
|
||||||
$scope.robotFinished = function(robot) {
|
$scope.robotFinished = function(robot) {
|
||||||
$scope.robotCreated({'robot': robot});
|
$scope.robotCreated({'robot': robot});
|
||||||
|
|
|
@ -12,8 +12,8 @@ angular.module('quay').directive('createTeamDialog', function () {
|
||||||
'info': '=info',
|
'info': '=info',
|
||||||
'teamCreated': '&teamCreated'
|
'teamCreated': '&teamCreated'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ApiService, UserService) {
|
controller: function($scope, $element, ApiService, UserService, NAME_PATTERNS) {
|
||||||
$scope.TEAM_PATTERN = TEAM_PATTERN;
|
$scope.TEAM_PATTERN = NAME_PATTERNS.TEAM_PATTERN;
|
||||||
|
|
||||||
$scope.teamFinished = function(team) {
|
$scope.teamFinished = function(team) {
|
||||||
$scope.teamCreated({'team': team});
|
$scope.teamCreated({'team': team});
|
||||||
|
|
|
@ -13,7 +13,7 @@ angular.module('quay').directive('imageFeatureView', function () {
|
||||||
'image': '=image',
|
'image': '=image',
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService, TableService) {
|
controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, ImageMetadataService, TableService) {
|
||||||
$scope.options = {
|
$scope.options = {
|
||||||
'filter': null,
|
'filter': null,
|
||||||
'predicate': 'fixableScore',
|
'predicate': 'fixableScore',
|
||||||
|
|
|
@ -13,7 +13,7 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
|
||||||
'image': '=image',
|
'image': '=image',
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService, TableService) {
|
controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, ImageMetadataService, TableService) {
|
||||||
$scope.options = {
|
$scope.options = {
|
||||||
'filter': null,
|
'filter': null,
|
||||||
'fixableVulns': false,
|
'fixableVulns': false,
|
||||||
|
|
|
@ -15,9 +15,9 @@ angular.module('quay').directive('namespaceInput', function () {
|
||||||
|
|
||||||
'namespaceTitle': '@namespaceTitle',
|
'namespaceTitle': '@namespaceTitle',
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element, NAME_PATTERNS) {
|
||||||
$scope.USERNAME_PATTERN = USERNAME_PATTERN;
|
$scope.USERNAME_PATTERN = NAME_PATTERNS.USERNAME_PATTERN;
|
||||||
$scope.usernamePattern = new RegExp(USERNAME_PATTERN);
|
$scope.usernamePattern = new RegExp(NAME_PATTERNS.USERNAME_PATTERN);
|
||||||
|
|
||||||
$scope.$watch('binding', function(binding) {
|
$scope.$watch('binding', function(binding) {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
|
|
12
static/js/quay-pages.module.ts
Normal file
12
static/js/quay-pages.module.ts
Normal file
|
@ -0,0 +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)
|
||||||
|
.directive('rpHeader', rpHeaderDirective)
|
||||||
|
.directive('rpSidebar', rpSidebarDirective)
|
||||||
|
.directive('rpBody', rpBodyDirective)
|
||||||
|
.name;
|
67
static/js/quay.config.ts
Normal file
67
static/js/quay.config.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
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}});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
59
static/js/quay.module.ts
Normal file
59
static/js/quay.module.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
|
||||||
|
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 (INJECTED_CONFIG && (INJECTED_CONFIG.MIXPANEL_KEY ||
|
||||||
|
INJECTED_CONFIG.MUNCHKIN_KEY ||
|
||||||
|
INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY)) {
|
||||||
|
quayDependencies.push('angulartics');
|
||||||
|
}
|
||||||
|
if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) {
|
||||||
|
quayDependencies.push('angulartics.mixpanel');
|
||||||
|
}
|
||||||
|
if (INJECTED_CONFIG && INJECTED_CONFIG.MUNCHKIN_KEY) {
|
||||||
|
quayDependencies.push('angulartics.marketo');
|
||||||
|
}
|
||||||
|
if (INJECTED_CONFIG && INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY) {
|
||||||
|
quayDependencies.push('angulartics.google.analytics');
|
||||||
|
}
|
||||||
|
if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) {
|
||||||
|
quayDependencies.push('vcRecaptcha');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default angular
|
||||||
|
.module('quay', quayDependencies)
|
||||||
|
.config(quayConfig)
|
||||||
|
.config(routeConfig)
|
||||||
|
.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;
|
135
static/js/quay.routes.ts
Normal file
135
static/js/quay.routes.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
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')
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
143
static/js/quay.run.ts
Normal file
143
static/js/quay.run.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
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': (<any>window).__token || ''});
|
||||||
|
|
||||||
|
// Handle session expiration.
|
||||||
|
Restangular.setErrorInterceptor(function(response) {
|
||||||
|
if (response !== undefined && response.status == 503) {
|
||||||
|
(<any>$('#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) {
|
||||||
|
(<any>$('#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((<any>window).location.href) == 0) {
|
||||||
|
(<any>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 <meta> 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;
|
||||||
|
(<any>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;
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
|
@ -1,144 +0,0 @@
|
||||||
describe("Service: AngularViewArray", function() {
|
|
||||||
var angularViewArray;
|
|
||||||
|
|
||||||
beforeEach(module('quay'));
|
|
||||||
|
|
||||||
beforeEach(inject(function($injector) {
|
|
||||||
angularViewArray = $injector.get('AngularViewArray');
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("create", function() {
|
|
||||||
|
|
||||||
it("returns a ViewArray object", function() {
|
|
||||||
var viewArray = angularViewArray.create();
|
|
||||||
|
|
||||||
expect(viewArray).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("returned ViewArray object", function() {
|
|
||||||
var viewArray;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
viewArray = angularViewArray.create();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("constructor", function() {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("length", function() {
|
|
||||||
|
|
||||||
it("returns the number of entries", function() {
|
|
||||||
viewArray.entries = [{}, {}, {}];
|
|
||||||
|
|
||||||
expect(viewArray.length()).toEqual(viewArray.entries.length);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("get", function() {
|
|
||||||
|
|
||||||
it("returns the entry at a given index", function() {
|
|
||||||
var index = 8;
|
|
||||||
viewArray.entries = new Array(10);
|
|
||||||
viewArray.entries[index] = 3;
|
|
||||||
|
|
||||||
expect(viewArray.get(index)).toEqual(viewArray.entries[index]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("push", function() {
|
|
||||||
|
|
||||||
it("adds given element to the end of entries", function() {
|
|
||||||
var element = 3;
|
|
||||||
var originalLength = viewArray.length();
|
|
||||||
viewArray.push(element);
|
|
||||||
|
|
||||||
expect(viewArray.entries.length).toEqual(originalLength + 1);
|
|
||||||
expect(viewArray.get(originalLength)).toEqual(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets 'hasEntries' to true", function() {
|
|
||||||
viewArray.push(2);
|
|
||||||
|
|
||||||
expect(viewArray.hasEntries).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("starts timer if 'isVisible' is true", function() {
|
|
||||||
spyOn(viewArray, "startTimer_").and.returnValue();
|
|
||||||
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();
|
|
||||||
viewArray.isVisible = false;
|
|
||||||
viewArray.push(2);
|
|
||||||
|
|
||||||
expect(viewArray.startTimer_).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("toggle", function() {
|
|
||||||
|
|
||||||
it("sets 'isVisible' to false if currently true", function() {
|
|
||||||
viewArray.isVisible = true;
|
|
||||||
viewArray.toggle();
|
|
||||||
|
|
||||||
expect(viewArray.isVisible).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets 'isVisible' to true if currently false", function() {
|
|
||||||
viewArray.isVisible = false;
|
|
||||||
viewArray.toggle();
|
|
||||||
|
|
||||||
expect(viewArray.isVisible).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("setVisible", function() {
|
|
||||||
|
|
||||||
it("sets 'isVisible' to false if given false", function() {
|
|
||||||
viewArray.setVisible(false);
|
|
||||||
|
|
||||||
expect(viewArray.isVisible).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets 'visibleEntries' to empty array if given false", function() {
|
|
||||||
viewArray.setVisible(false);
|
|
||||||
|
|
||||||
expect(viewArray.visibleEntries.length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows additional entries if given true", function() {
|
|
||||||
spyOn(viewArray, "showAdditionalEntries_").and.returnValue();
|
|
||||||
viewArray.setVisible(true);
|
|
||||||
|
|
||||||
expect(viewArray.showAdditionalEntries_).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not show additional entries if given false", function() {
|
|
||||||
spyOn(viewArray, "showAdditionalEntries_").and.returnValue();
|
|
||||||
viewArray.setVisible(false);
|
|
||||||
|
|
||||||
expect(viewArray.showAdditionalEntries_).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("starts timer if given true", function() {
|
|
||||||
spyOn(viewArray, "startTimer_").and.returnValue();
|
|
||||||
viewArray.setVisible(true);
|
|
||||||
|
|
||||||
expect(viewArray.startTimer_).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stops timer if given false", function() {
|
|
||||||
spyOn(viewArray, "stopTimer_").and.returnValue();
|
|
||||||
viewArray.setVisible(true);
|
|
||||||
|
|
||||||
expect(viewArray.stopTimer_).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { RouteBuilder } from './route-builder.service';
|
||||||
|
|
||||||
|
|
||||||
|
export class RouteBuilderImpl implements RouteBuilder {
|
||||||
|
|
||||||
|
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 == this.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 = page.flags || {};
|
||||||
|
options['templateUrl'] = templateUrl;
|
||||||
|
options['reloadOnSearch'] = false;
|
||||||
|
options['controller'] = page.controller;
|
||||||
|
|
||||||
|
this.routeProvider.when(path, options);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,60 +1,57 @@
|
||||||
describe("Service: RouteBuilder", function() {
|
import { RouteBuilderImpl } from './route-builder.service.impl';
|
||||||
var RouteBuilder;
|
|
||||||
var routeProviderMock;
|
|
||||||
var pagesMock;
|
|
||||||
var profiles;
|
|
||||||
var currentProfile;
|
|
||||||
|
|
||||||
beforeEach(module('quay'));
|
|
||||||
|
|
||||||
beforeEach(inject(function($injector) {
|
describe("Service: RouteBuilderImpl", () => {
|
||||||
|
var routeProviderMock: any;
|
||||||
|
var pagesMock: any;
|
||||||
|
var profiles: any[];
|
||||||
|
|
||||||
|
beforeEach((() => {
|
||||||
profiles = [
|
profiles = [
|
||||||
{id: 'old-layout', templatePath: '/static/partials/'},
|
{id: 'old-layout', templatePath: '/static/partials/'},
|
||||||
{id: 'layout', templatePath: '/static/partials/'}
|
{id: 'layout', templatePath: '/static/partials/'}
|
||||||
];
|
];
|
||||||
currentProfile = 'layout';
|
|
||||||
routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']);
|
routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']);
|
||||||
pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']);
|
pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']);
|
||||||
RouteBuilder = $injector.get('RouteBuilder');
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("constructor", function() {
|
describe("constructor", () => {
|
||||||
|
|
||||||
it("returns a RouteBuilder object", function() {
|
it("returns a RouteBuilder object", () => {
|
||||||
var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile);
|
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
|
||||||
|
|
||||||
expect(routeBuilder).toBeDefined();
|
expect(routeBuilder).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("initializes dependencies", function() {
|
it("initializes current profile to 'layout'", () => {
|
||||||
var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile);
|
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
|
||||||
|
|
||||||
expect(routeBuilder.routeProvider).toEqual(routeProviderMock);
|
expect(routeBuilder.currentProfile).toEqual('layout');
|
||||||
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() {
|
it("initializes available profiles", () => {
|
||||||
var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, 'fake-profile');
|
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
|
||||||
|
var matchingRoutes: any[] = routeBuilder.profiles.filter((profile) => {
|
||||||
expect(routeBuilder.profiles).toEqual(profiles);
|
return profiles.indexOf(profile) == -1;
|
||||||
|
});
|
||||||
|
expect(matchingRoutes).toEqual(routeBuilder.profiles);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets 'profiles' to the first given profile with id matching given current profile", function() {
|
it("sets 'profiles' to the first given profile with id matching given current profile", () => {
|
||||||
var routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile);
|
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
|
||||||
|
|
||||||
expect(routeBuilder.profiles).toEqual([profiles[1]]);
|
expect(routeBuilder.profiles).toEqual([profiles[1]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("otherwise", function() {
|
describe("otherwise", () => {
|
||||||
var routeBuilder;
|
var routeBuilder: RouteBuilderImpl;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile);
|
routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls routeProvider to set fallback route with given options", function() {
|
it("calls routeProvider to set fallback route with given options", () => {
|
||||||
var options = {1: "option"};
|
var options = {1: "option"};
|
||||||
routeBuilder.otherwise(options);
|
routeBuilder.otherwise(options);
|
||||||
|
|
||||||
|
@ -62,13 +59,13 @@ describe("Service: RouteBuilder", function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("route", function() {
|
describe("route", () => {
|
||||||
var routeBuilder;
|
var routeBuilder: RouteBuilderImpl;
|
||||||
var path;
|
var path: string;
|
||||||
var pagename;
|
var pagename: string;
|
||||||
var page;
|
var page: any;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
path = '/repository/:namespace/:name';
|
path = '/repository/:namespace/:name';
|
||||||
pagename = 'repo-view';
|
pagename = 'repo-view';
|
||||||
page = {
|
page = {
|
||||||
|
@ -77,10 +74,10 @@ describe("Service: RouteBuilder", function() {
|
||||||
controller: jasmine.createSpy('pageController'),
|
controller: jasmine.createSpy('pageController'),
|
||||||
flags: {},
|
flags: {},
|
||||||
};
|
};
|
||||||
routeBuilder = new RouteBuilder(routeProviderMock, pagesMock, profiles, currentProfile);
|
routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls pages with given pagename and 'profiles' to get matching page and profile pair", function() {
|
it("calls pages with given pagename and 'profiles' to get matching page and profile pair", () => {
|
||||||
pagesMock.get.and.returnValue([profiles[1], page]);
|
pagesMock.get.and.returnValue([profiles[1], page]);
|
||||||
routeBuilder.route(path, pagename);
|
routeBuilder.route(path, pagename);
|
||||||
|
|
||||||
|
@ -88,7 +85,7 @@ describe("Service: RouteBuilder", function() {
|
||||||
expect(pagesMock.get.calls.argsFor(0)[1]).toEqual(routeBuilder.profiles);
|
expect(pagesMock.get.calls.argsFor(0)[1]).toEqual(routeBuilder.profiles);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error if no matching page/profile pair found", function() {
|
it("throws error if no matching page/profile pair found", () => {
|
||||||
pagesMock.get.and.returnValue();
|
pagesMock.get.and.returnValue();
|
||||||
try {
|
try {
|
||||||
routeBuilder.route(path, pagename);
|
routeBuilder.route(path, pagename);
|
||||||
|
@ -98,9 +95,9 @@ describe("Service: RouteBuilder", function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls routeProvider to set route for given path and options", function() {
|
it("calls routeProvider to set route for given path and options", () => {
|
||||||
pagesMock.get.and.returnValue([profiles[1], page]);
|
pagesMock.get.and.returnValue([profiles[1], page]);
|
||||||
var expectedOptions = {
|
var expectedOptions: any = {
|
||||||
templateUrl: profiles[1].templatePath + page.templateName,
|
templateUrl: profiles[1].templatePath + page.templateName,
|
||||||
reloadOnSearch: false,
|
reloadOnSearch: false,
|
||||||
controller: page.controller,
|
controller: page.controller,
|
||||||
|
@ -111,7 +108,7 @@ describe("Service: RouteBuilder", function() {
|
||||||
expect(routeProviderMock.when.calls.argsFor(0)[1]).toEqual(expectedOptions);
|
expect(routeProviderMock.when.calls.argsFor(0)[1]).toEqual(expectedOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns itself (the RouteBuilder instance)", function() {
|
it("returns itself (the RouteBuilder instance)", () => {
|
||||||
pagesMock.get.and.returnValue([profiles[1], page]);
|
pagesMock.get.and.returnValue([profiles[1], page]);
|
||||||
|
|
||||||
expect(routeBuilder.route(path, pagename)).toEqual(routeBuilder);
|
expect(routeBuilder.route(path, pagename)).toEqual(routeBuilder);
|
18
static/js/services/route-builder/route-builder.service.ts
Normal file
18
static/js/services/route-builder/route-builder.service.ts
Normal file
|
@ -0,0 +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;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Service which provides helper methods for constructing and managing tabular data.
|
* Service which provides helper methods for constructing and managing tabular data.
|
||||||
*/
|
*/
|
||||||
angular.module('quay').factory('TableService', ['AngularViewArray', function(AngularViewArray) {
|
angular.module('quay').factory('TableService', ['ViewArray', function(ViewArray) {
|
||||||
var tableService = {};
|
var tableService = {};
|
||||||
|
|
||||||
tableService.tablePredicateClass = function(name, predicate, reverse) {
|
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) {
|
tableService.buildOrderedItems = function(items, options, filterFields, numericFields, opt_extrafilter) {
|
||||||
var orderedItems = AngularViewArray.create();
|
var orderedItems = ViewArray.create();
|
||||||
|
|
||||||
items.forEach(function(item) {
|
items.forEach(function(item) {
|
||||||
var filter = options.filter;
|
var filter = options.filter;
|
||||||
|
|
147
static/js/services/view-array/view-array.impl.spec.ts
Normal file
147
static/js/services/view-array/view-array.impl.spec.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { ViewArrayImpl } from './view-array.impl';
|
||||||
|
|
||||||
|
|
||||||
|
describe("ViewArrayImplImpl", () => {
|
||||||
|
var viewArrayImpl: ViewArrayImpl;
|
||||||
|
var $intervalMock: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
$intervalMock = jasmine.createSpy('$intervalSpy');
|
||||||
|
$intervalMock.and.returnValue({});
|
||||||
|
$intervalMock.cancel = jasmine.createSpy('cancelSpy');
|
||||||
|
viewArrayImpl = new ViewArrayImpl($intervalMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
|
||||||
|
it("returns a new ViewArrayImpl instance", () => {
|
||||||
|
var newViewArrayImpl: ViewArrayImpl = viewArrayImpl.create();
|
||||||
|
|
||||||
|
expect(newViewArrayImpl).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
95
static/js/services/view-array/view-array.impl.ts
Normal file
95
static/js/services/view-array/view-array.impl.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { ViewArray } from './view-array';
|
||||||
|
import { Inject } from '../../decorators/inject/inject.decorator';
|
||||||
|
|
||||||
|
|
||||||
|
export class ViewArrayImpl implements ViewArray {
|
||||||
|
|
||||||
|
public entries: any[];
|
||||||
|
public isVisible: boolean;
|
||||||
|
public visibleEntries: any[];
|
||||||
|
public hasEntries: boolean;
|
||||||
|
public hasHiddenEntries: boolean;
|
||||||
|
private timerRef: any;
|
||||||
|
private currentIndex: number;
|
||||||
|
private additionalCount: number = 20;
|
||||||
|
|
||||||
|
constructor(@Inject('$interval') private interval: any) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timerRef = this.interval(() => {
|
||||||
|
this.showAdditionalEntries();
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTimer(): void {
|
||||||
|
if (this.timerRef) {
|
||||||
|
this.interval.cancel(this.timerRef);
|
||||||
|
this.timerRef = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
static/js/services/view-array/view-array.ts
Normal file
69
static/js/services/view-array/view-array.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
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
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a new ViewArray.
|
||||||
|
* @return viewArray New ViewArray instance.
|
||||||
|
*/
|
||||||
|
public abstract create(): ViewArrayImpl;
|
||||||
|
}
|
6
static/test/jasmine.json
Normal file
6
static/test/jasmine.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"spec_dir": "./static/js",
|
||||||
|
"spec_files": [
|
||||||
|
"**/*.spec.js"
|
||||||
|
]
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"outDir": "./build/",
|
"outDir": "./build/",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"experimentalDecorators": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"sass/*": ["./static/css/directives/components/pages/*"]
|
"sass/*": ["./static/css/directives/components/pages/*"]
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
"node_modules"
|
"node_modules"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"./static/js/**/*.tsx"
|
"./static/js/**/*.tsx",
|
||||||
|
"./static/js/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ var webpack = require('webpack');
|
||||||
var path = require("path");
|
var path = require("path");
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
entry: ["./static/js/app.tsx"],
|
entry: "./static/js/quay.module.ts",
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, "static/js/build"),
|
path: path.resolve(__dirname, "static/build"),
|
||||||
filename: "bundle.js"
|
filename: "bundle.js"
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
@ -13,6 +13,10 @@ var config = {
|
||||||
"sass": path.resolve('./static/css/directives/components/pages/')
|
"sass": path.resolve('./static/css/directives/components/pages/')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Use window.angular to maintain compatibility with non-Webpack components
|
||||||
|
externals: {
|
||||||
|
"angular": "angular",
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
loaders: [
|
||||||
{
|
{
|
||||||
|
@ -24,9 +28,14 @@ var config = {
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
loaders: ['style', 'css', 'sass'],
|
loaders: ['style', 'css', 'sass'],
|
||||||
exclude: /node_modules/
|
exclude: /node_modules/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /angular\.js$/,
|
||||||
|
loader: 'expose?angular',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
devtool: "source-map",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
Reference in a new issue