var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$'; var USER_PATTERN = '^[a-z0-9_]{4,30}$'; // 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']; if (window.__config && window.__config.MIXPANEL_KEY) { quayDependencies.push('angulartics'); quayDependencies.push('angulartics.mixpanel'); } // Define the application. quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) { cfpLoadingBarProvider.includeSpinner = false; }); // Configure the routes. quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeProvider, $locationProvider, pages) { var title = window.__config['REGISTRY_TITLE'] || 'Quay.io'; $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 = 'old-layout'; // Check for the cookie for turning on the new layout. if (document.cookie.toString().indexOf('quay.exp-new-layout=true') >= 0) { layoutProfile = 'layout'; } // Check for the override flag. if (window.location.search.indexOf('old-ui=1') >= 0) { layoutProfile = 'old-layout'; } window.console.log('Using layout profile: ' + layoutProfile); var routeBuilder = new AngularRouteBuilder($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); 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 Admin .route('/repository/:namespace/:name/admin', 'repo-admin') // Repo Builds .route('/repository/:namespace/:name/build', 'repo-build') // Repo Build View .route('/repository/:namespace/:name/build/:buildid', 'build-view') // Repo Build Package .route('/repository/:namespace/:name/build/:buildid/buildpack', 'build-package') // Repo List .route('/repository/', 'repo-list') // Organizations .route('/organizations/', 'organizations') // New Organization .route('/organizations/new/', 'new-organization') // View Organization .route('/organization/:orgname', 'org-view') // Organization Admin .route('/organization/:orgname/admin', 'org-admin') // View Organization Team .route('/organization/:orgname/teams/:teamname', 'team-view') // Organization Member Logs .route('/organization/:orgname/logs/:membername', 'org-member-logs') // Organization View Application .route('/organization/:orgname/application/:clientid', 'manage-application') // View User .route('/user/:username', 'user-view') // DEPRECATED: User Admin .route('/user/', 'user-admin') // Sign In .route('/signin/', 'signin') // New Repository .route('/new/', 'new-repo') // ER Management .route('/superuser/', 'superuser') // ER Setup .route('/setup/', 'setup') // Plans .route('/plans/', 'plans') // Tutorial .route('/tutorial/', 'tutorial') // Contact .route('/contact/', 'contact') // About .route('/about/', 'about') // Security .route('/security/', 'security') // 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') // Enable/disable experimental layout .route('/__exp/newlayout', 'exp-new-layout') // Default: Redirect to the landing page .otherwise({redirectTo: '/'}); }]); // 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', function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService, MetaService) { var title = window.__config['REGISTRY_TITLE'] || 'Quay.io'; // Handle session security. Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], {'_csrf_token': window.__token || ''}); // Handle session expiration. Restangular.setErrorInterceptor(function(response) { if (response.status == 401 && response.data['error_type'] == 'invalid_token' && response.data['session_required'] !== false) { $('#sessionexpiredModal').modal({}); return false; } if (response.status == 503) { $('#cannotContactService').modal({}); return false; } if (response.status == 500) { document.location = '/500'; 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; } var changeTab = function(activeTab, opt_timeout) { var checkCount = 0; $timeout(function() { if (checkCount > 5) { return; } checkCount++; $('a[data-toggle="tab"]').each(function(index) { var tabName = this.getAttribute('data-target').substr(1); if (tabName != activeTab) { return; } if (this.clientWidth == 0) { changeTab(activeTab, 500); return; } UtilService.clickElement(this); }); }, opt_timeout); }; var resetDefaultTab = function() { $timeout(function() { $('a[data-toggle="tab"]').each(function(index) { if (index == 0) { UtilService.clickElement(this); } }); }); }; $rootScope.$watch('description', function(description) { if (!description) { description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.'; } // Note: We set the content of the description tag manually here rather than using Angular binding // because we need the tag to have a default description that is not of the form "{{ description }}", // we read by tools that do not properly invoke the Angular code. $('#descriptionTag').attr('content', description); }); $rootScope.$on('$routeUpdate', function(){ if ($location.search()['tab']) { changeTab($location.search()['tab']); } else { resetDefaultTab(); } }); $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { $rootScope.current = current.$$route; $rootScope.currentPage = current; $rootScope.pageClass = ''; if (!current.$$route) { return; } $rootScope.pageClass = current.$$route.pageClass || ''; $rootScope.newLayout = !!current.$$route.newLayout; $rootScope.fixFooter = !!current.$$route.fixFooter; MetaService.getInitialTitle(current, function(title) { $rootScope.title = title; }); MetaService.getInitialDescription(current, function(description) { $rootScope.description = description }); $anchorScroll(); }); $rootScope.$on('$viewContentLoaded', function(event) { var current = $rootScope.currentPage; MetaService.getTitle(current, function(title) { $rootScope.title = title; }); MetaService.getDescription(current, function(description) { $rootScope.description = description; }); var activeTab = $location.search()['tab']; var checkTabs = function() { var tabs = $('a[data-toggle="tab"]'); if (tabs.length == 0) { $timeout(checkTabs, 50); return; } tabs.on('shown.bs.tab', function (e) { var tabName = e.target.getAttribute('data-target').substr(1); $rootScope.$apply(function() { var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target; var newSearch = $.extend($location.search(), {}); if (isDefaultTab) { delete newSearch['tab']; } else { newSearch['tab'] = tabName; } $location.search(newSearch); }); e.preventDefault(); }); if (activeTab) { changeTab(activeTab); } }; // Setup deep linking of tabs. This will change the search field of the URL whenever a tab // is changed in the UI. $timeout(checkTabs, 50); }); var initallyChecked = false; window.__isLoading = function() { if (!initallyChecked) { initallyChecked = true; return true; } return $http.pendingRequests.length > 0; }; }]);