initial import for Open Source 🎉

This commit is contained in:
Jimmy Zelinskie 2019-11-12 11:09:47 -05:00
parent 1898c361f3
commit 9c0dd3b722
2048 changed files with 218743 additions and 0 deletions

17
static/js/pages/about.js Normal file
View file

@ -0,0 +1,17 @@
import billOfMaterials from "../../../bill-of-materials.json"
(function() {
/**
* About page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('about', 'about.html', AboutCtrl, {
'title': 'About Us',
'description': 'About Us'
});
}]);
function AboutCtrl($scope){
$scope.billOfMaterials = billOfMaterials
}
}());

View file

@ -0,0 +1,85 @@
(function() {
/**
* Application listing page. Shows all applications for all visibile namespaces.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('app-list', 'app-list.html', AppListCtrl, {
'newLayout': true,
'title': 'Applications',
'description': 'View and manage applications'
})
}]);
function AppListCtrl($scope, $sanitize, $q, Restangular, UserService, ApiService, Features,
StateService) {
$scope.namespace = null;
$scope.page = 1;
$scope.publicPageCount = null;
$scope.allRepositories = {};
$scope.loading = true;
$scope.resources = [];
$scope.Features = Features;
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
// When loading the UserService, if the user is logged in, create a list of
// relevant namespaces and collect the relevant repositories.
UserService.updateUserIn($scope, function(user) {
$scope.loading = false;
if (!user.anonymous) {
// Add our user to our list of namespaces.
$scope.namespaces = [{
'name': user.username,
'avatar': user.avatar
}];
// Add each org to our list of namespaces.
user.organizations.map(function(org) {
$scope.namespaces.push({
'name': org.name,
'avatar': org.avatar
});
});
// Load the repos.
loadRepos();
}
});
$scope.isOrganization = function(namespace) {
return !!UserService.getOrganization(namespace);
};
// Finds a duplicate repo if it exists. If it doesn't, inserts the repo.
var findDuplicateRepo = function(repo) {
var found = $scope.allRepositories[repo.namespace + '/' + repo.name];
if (found) {
return found;
} else {
$scope.allRepositories[repo.namespace + '/' + repo.name] = repo;
return repo;
}
};
var loadRepos = function() {
if (!$scope.user || $scope.user.anonymous || $scope.namespaces.length == 0) {
return;
}
$scope.namespaces.map(function(namespace) {
var options = {
'namespace': namespace.name,
'last_modified': true,
'popularity': true,
'repo_kind': 'application',
'public': true,
};
namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories.map(findDuplicateRepo);
});
$scope.resources.push(namespace.repositories);
});
};
}
})();

View file

@ -0,0 +1,45 @@
(function() {
/**
* Application view page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('app-view', 'app-view.html', AppViewCtrl, {
'newLayout': true,
'title': '{{ namespace }}/{{ name }}',
'description': 'Application {{ namespace }}/{{ name }}'
});
}]);
function AppViewCtrl($scope, $routeParams, $rootScope, ApiService, UtilService) {
$scope.namespace = $routeParams.namespace;
$scope.name = $routeParams.name;
$scope.viewScope = {};
$scope.settingsShown = 0;
$scope.showSettings = function() {
$scope.settingsShown++;
};
var loadRepository = function() {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'repo_kind': 'application',
'includeStats': true,
'includeTags': false
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
if (repo != undefined) {
$scope.repository = repo;
$scope.viewScope.repository = repo;
// Update the page description for SEO
$rootScope.description = UtilService.getFirstMarkdownLineAsString(repo.description);
}
});
};
loadRepository();
}
})();

View file

@ -0,0 +1,47 @@
(function() {
/**
* Billing plans page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('billing', 'billing.html', BillingCtrl, {
'title': 'Billing',
'description': 'Billing',
'newLayout': true
});
}]);
/**
* Billing invoices page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('invoices', 'invoices.html', BillingCtrl, {
'title': 'Billing Invoices',
'description': 'Billing Invoices',
'newLayout': true
});
}]);
function BillingCtrl($scope, ApiService, $routeParams, UserService) {
$scope.orgname = $routeParams['orgname'];
$scope.username = $routeParams['username'];
var loadEntity = function() {
if ($scope.orgname) {
$scope.entityResource = ApiService.getOrganizationAsResource({'orgname': $scope.orgname}).get(function(org) {
$scope.organization = org;
});
} else {
UserService.updateUserIn($scope, function(currentUser) {
$scope.entityResource = ApiService.getUserInformationAsResource({'username': $scope.username}).get(function(user) {
$scope.invaliduser = !currentUser || currentUser.username != $scope.username;
$scope.viewuser = user;
});
});
}
};
// Load the user or organization.
loadEntity();
}
}());

View file

@ -0,0 +1,78 @@
(function() {
/**
* Build view page. Displays the view of a particular build for a repository.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('build-view', 'build-view.html', BuildViewCtrl, {
newLayout: true,
title: 'Build {{ build.display_name }}',
description: 'Logs and status for build {{ build.display_name }}'
});
}]);
function BuildViewCtrl($scope, ApiService, $routeParams, AngularPollChannel, CookieService,
$location, StateService) {
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
$scope.namespace = $routeParams.namespace;
$scope.name = $routeParams.name;
$scope.build_uuid = $routeParams.buildid;
if (!CookieService.get('quay.showBuildLogTimestamps')) {
$scope.showLogTimestamps = true;
} else {
$scope.showLogTimestamps = CookieService.get('quay.showBuildLogTimestamps') == 'true';
}
var loadBuild = function() {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'build_uuid': $scope.build_uuid
};
$scope.buildResource = ApiService.getRepoBuildAsResource(params).get(function(build) {
$scope.build = build;
$scope.originalBuild = build;
});
};
var loadRepository = function() {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'includeTags': false
};
$scope.repoResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repo = repo;
}, ApiService.errorDisplay('Cannot load repository'));
};
// Page startup:
loadRepository();
loadBuild();
$scope.askCancelBuild = function(build) {
bootbox.confirm('Are you sure you want to cancel this build?', function(r) {
if (r) {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'build_uuid': build.id
};
ApiService.cancelRepoBuild(null, params).then(function () {
$location.path('/repository/' + $scope.namespace + '/' + $scope.name);
}, ApiService.errorDisplay('Cannot cancel build'));
}
});
};
$scope.toggleTimestamps = function() {
$scope.showLogTimestamps = !$scope.showLogTimestamps;
CookieService.putPermanent('quay.showBuildLogTimestamps', $scope.showLogTimestamps);
};
$scope.setUpdatedBuild = function(build) {
$scope.build = build;
};
}
})();

View file

@ -0,0 +1,40 @@
(function() {
/**
* Page for confirming an invite to a team.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('confirm-invite', 'confirm-invite.html', ConfirmInviteCtrl, {
'title': 'Confirm Invitation'
});
}]);
function ConfirmInviteCtrl($scope, $location, UserService, ApiService, NotificationService) {
// Monitor any user changes and place the current user into the scope.
$scope.loading = false;
$scope.inviteCode = $location.search()['code'] || '';
UserService.updateUserIn($scope, function(user) {
if (!user.anonymous && !$scope.loading) {
// Make sure to not redirect now that we have logged in. We'll conduct the redirect
// manually.
$scope.redirectUrl = null;
$scope.loading = true;
var params = {
'code': $location.search()['code']
};
ApiService.acceptOrganizationTeamInvite(null, params).then(function(resp) {
NotificationService.update();
UserService.load();
$location.path('/organization/' + resp.org + '/teams/' + resp.team);
}, function(resp) {
$scope.loading = false;
$scope.invalid = ApiService.getErrorMessage(resp, 'Invalid confirmation code');
});
}
});
$scope.redirectUrl = window.location.href;
}
})();

View file

@ -0,0 +1,55 @@
(function() {
/**
* Contact details page. The contacts are configurable.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('contact', 'contact.html', ContactCtrl, {
'title': 'Contact Us'
});
}]);
function ContactCtrl($scope, Config) {
$scope.Config = Config;
$scope.colsize = Math.floor(12 / Config.CONTACT_INFO.length);
$scope.getKind = function(contactInfo) {
var colon = contactInfo.indexOf(':');
var scheme = contactInfo.substr(0, colon);
if (scheme == 'https' || scheme == 'http') {
if (contactInfo.indexOf('//twitter.com/') > 0) {
return 'twitter';
}
return 'url';
}
return scheme;
};
$scope.getTitle = function(contactInfo) {
switch ($scope.getKind(contactInfo)) {
case 'url':
return contactInfo;
case 'twitter':
var parts = contactInfo.split('/');
return '@' + parts[parts.length - 1];
case 'tel':
return contactInfo.substr('tel:'.length);
case 'irc':
// irc://chat.freenode.net:6665/quayio
var parts = contactInfo.substr('irc://'.length).split('/');
var server = parts[0];
if (server.indexOf('freenode') > 0) {
server = 'Freenode';
}
return server + ': #' + parts[parts.length - 1];
case 'mailto':
return contactInfo.substr('mailto:'.length);
}
}
}
})();

View file

@ -0,0 +1,34 @@
(function() {
/**
* Create repository notification page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('create-repository-notification', 'create-repository-notification.html', CreateRepoNotificationCtrl, {
'newLayout': true,
'title': 'Create Repo Notification: {{ namespace }}/{{ name }}',
'description': 'Create repository notification for repository {{ namespace }}/{{ name }}'
})
}]);
function CreateRepoNotificationCtrl($scope, $routeParams, $location, ApiService) {
$scope.namespace = $routeParams.namespace;
$scope.name = $routeParams.name;
var loadRepository = function() {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'includeTags': false
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repository = repo;
});
};
loadRepository();
$scope.notificationCreated = function() {
$location.url('repository/' + $scope.namespace + '/' + $scope.name + '?tab=settings');
};
}
})();

View file

@ -0,0 +1,17 @@
(function() {
/**
* Error view page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('error-view', 'error-view.html', ErrorViewCtrl, {
'title': '{{info.error_message || "Error"}}',
'description': 'Error',
'newLayout': false
});
}]);
function ErrorViewCtrl($scope, ApiService, $routeParams, $rootScope, UserService) {
$scope.info = window.__error_info;
$scope.code = window.__error_code || 404;
}
}());

View file

@ -0,0 +1,86 @@
(function() {
/**
* The Incomplete Setup page provides information to the user about what's wrong with the current configuration
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('incomplete-setup', 'incomplete-setup.html', IncompleteSetupCtrl,
{
'newLayout': true,
'title': 'Red Hat Quay Setup Incomplete'
})
}]);
function IncompleteSetupCtrl($scope, $location, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog, Config) {
if (Config['SETUP_COMPLETE']) {
$location.path('/');
return;
}
if (!Features.SUPER_USERS) {
return;
}
$scope.States = {
// Loading the state of the product.
'LOADING': 'loading',
// The configuration directory is missing.
'MISSING_CONFIG_DIR': 'missing-config-dir',
// The config.yaml exists but it is invalid.
'INVALID_CONFIG': 'config-invalid',
};
$scope.currentStep = $scope.States.LOADING;
$scope.$watch('currentStep', function(currentStep) {
switch (currentStep) {
case $scope.States.MISSING_CONFIG_DIR:
$scope.showMissingConfigDialog();
break;
case $scope.States.INVALID_CONFIG:
$scope.showInvalidConfigDialog();
break;
}
});
$scope.showInvalidConfigDialog = function() {
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
var title = "Invalid configuration file";
CoreDialog.fatal(title, message);
};
$scope.showMissingConfigDialog = function() {
var title = "Missing configuration volume";
var message = "It looks like Quay was not mounted with a configuration volume. The volume should be " +
"mounted into the container at <code>/conf/stack</code>. " +
"<br>If you have a tarball, please ensure you untar it into a directory and re-run this container with: " +
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
"<br>If you haven't configured your Quay instance, please run the container with: " +
"<br><br><pre>docker run &lt;name-of-image&gt; config </pre>" +
"For more information, " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Read the Setup Guide</a>";
if (window.__kubernetes_namespace) {
title = "Configuration Secret Missing";
message = `It looks like the Red Hat Quay secret is not present in the namespace <code>${window.__kubernetes_namespace}.</code>` +
"<br>Please double-check that the secret exists, or " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"refer to the Setup Guide</a>";
}
CoreDialog.fatal(title, message);
};
$scope.checkStatus = function() {
ContainerService.checkStatus(function(resp) {
$scope.currentStep = resp['status'];
}, $scope.currentConfig);
};
// Load the initial status.
$scope.checkStatus();
};
})();

View file

@ -0,0 +1,90 @@
(function() {
/**
* Landing page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('landing', 'landing.html', LandingCtrl, {
'pageClass': function(Features) {
return Features.BILLING ? 'landing-page' : '';
}
});
}]);
function LandingCtrl($scope, $location, UserService, ApiService, Features, Config) {
$scope.currentScreenshot = 'repo-view';
$scope.userRegistered = false;
if (!Config['SETUP_COMPLETE'] && !Features.BILLING) {
$location.path('/incomplete-setup');
return;
}
UserService.updateUserIn($scope, function(user) {
if (!user.anonymous) {
if (user.prompts && user.prompts.length) {
$location.path('/updateuser/');
} else {
$location.path('/repository/');
}
}
});
$scope.handleUserRegistered = function() {
$scope.userRegistered = true;
};
$scope.changeScreenshot = function(screenshot) {
$scope.currentScreenshot = screenshot;
};
$scope.chromify = function() {
browserchrome.update();
var jcarousel = $('.jcarousel');
jcarousel
.on('jcarousel:reload jcarousel:create', function () {
var width = jcarousel.innerWidth();
jcarousel.jcarousel('items').css('width', width + 'px');
})
.jcarousel({
wrap: 'circular'
});
$('.jcarousel-control-prev')
.on('jcarouselcontrol:active', function() {
$(this).removeClass('inactive');
})
.on('jcarouselcontrol:inactive', function() {
$(this).addClass('inactive');
})
.jcarouselControl({
target: '-=1'
});
$('.jcarousel-control-next')
.on('jcarouselcontrol:active', function() {
$(this).removeClass('inactive');
})
.on('jcarouselcontrol:inactive', function() {
$(this).addClass('inactive');
})
.jcarouselControl({
target: '+=1'
});
$('.jcarousel-pagination')
.on('jcarouselpagination:active', 'a', function() {
$(this).addClass('active');
})
.on('jcarouselpagination:inactive', 'a', function() {
$(this).removeClass('active');
})
.jcarouselPagination({
'item': function(page, carouselItems) {
return '<a href="javascript:void(0)" class="jcarousel-page"></a>';
}
});
};
}
})();

View file

@ -0,0 +1,124 @@
(function() {
/**
* Page for managing an organization-defined OAuth application.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('manage-application', 'manage-application.html', ManageApplicationCtrl, {
'newLayout': true,
'title': 'Manage Application {{ application.name }}',
'description': 'Manage an OAuth application'
});
}]);
function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, OAuthService, ApiService, UserService, Config) {
var orgname = $routeParams.orgname;
var clientId = $routeParams.clientid;
$scope.Config = Config;
$scope.OAuthService = OAuthService;
$scope.updating = false;
$scope.genScopes = {};
UserService.updateUserIn($scope);
$scope.getScopes = function(scopes) {
var checked = [];
for (var scopeName in scopes) {
if (scopes.hasOwnProperty(scopeName) && scopes[scopeName]) {
checked.push(scopeName);
}
}
return checked;
};
$scope.askResetClientSecret = function() {
$('#resetSecretModal').modal({});
};
$scope.askDelete = function() {
$('#deleteAppModal').modal({});
};
$scope.deleteApplication = function() {
var params = {
'orgname': orgname,
'client_id': clientId
};
$('#deleteAppModal').modal('hide');
ApiService.deleteOrganizationApplication(null, params).then(function(resp) {
$timeout(function() {
$location.path('/organization/' + orgname + '/admin');
}, 500);
}, ApiService.errorDisplay('Could not delete application'));
};
$scope.updateApplication = function() {
$scope.updating = true;
var params = {
'orgname': orgname,
'client_id': clientId
};
if (!$scope.application['description']) {
delete $scope.application['description'];
}
if (!$scope.application['avatar_email']) {
delete $scope.application['avatar_email'];
}
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {
$scope.updating = false;
});
ApiService.updateOrganizationApplication($scope.application, params).then(function(resp) {
$scope.application = resp;
}, errorHandler);
};
$scope.resetClientSecret = function() {
var params = {
'orgname': orgname,
'client_id': clientId
};
$('#resetSecretModal').modal('hide');
ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) {
$scope.application = resp;
}, ApiService.errorDisplay('Could not reset client secret'));
};
var loadOrganization = function() {
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org;
return org;
});
};
var loadApplicationInfo = function() {
var params = {
'orgname': orgname,
'client_id': clientId
};
$scope.appResource = ApiService.getOrganizationApplicationAsResource(params).get(function(resp) {
$scope.application = resp;
$rootScope.title = 'Manage Application ' + $scope.application.name + ' (' + $scope.orgname + ')';
$rootScope.description = 'Manage the details of application ' + $scope.application.name +
' under organization ' + $scope.orgname;
return resp;
});
};
// Load the organization and application info.
loadOrganization();
loadApplicationInfo();
}
})();

View file

@ -0,0 +1,86 @@
(function() {
/**
* Page to view the details of a single manifest.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('manifest-view', 'manifest-view.html', ManifestViewCtrl, {
'newLayout': true,
'title': '{{ manifest_digest }}',
'description': 'Manifest {{ manifest_digest }}'
})
}]);
function ManifestViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService, Features, CookieService) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var manifest_digest = $routeParams.manifest_digest;
$scope.manifestSecurityCounter = 0;
$scope.manifestPackageCounter = 0;
$scope.options = {
'vulnFilter': ''
};
var loadManifest = function() {
var params = {
'repository': namespace + '/' + name,
'manifestref': manifest_digest
};
$scope.manifestResource = ApiService.getRepoManifestAsResource(params).get(function(manifest) {
$scope.manifest = manifest;
$scope.reversedLayers = manifest.layers ? manifest.layers.reverse() : null;
});
};
var loadRepository = function() {
var params = {
'repository': namespace + '/' + name,
'includeTags': false
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repository = repo;
});
};
loadManifest();
loadRepository();
$scope.loadManifestSecurity = function() {
if (!Features.SECURITY_SCANNER) { return; }
$scope.manifestSecurityCounter++;
};
$scope.loadManifestPackages = function() {
if (!Features.SECURITY_SCANNER) { return; }
$scope.manifestPackageCounter++;
};
$scope.manifestsOf = function(manifest) {
if (!manifest || !manifest.is_manifest_list) {
return [];
}
if (!manifest._mapped_manifests) {
// Calculate once and cache to avoid angular digest cycles.
var parsed_manifest = JSON.parse(manifest.manifest_data);
manifest._mapped_manifests = parsed_manifest.manifests.map(function(manifest) {
return {
'repository': $scope.repository,
'raw': manifest,
'os': manifest.platform.os,
'architecture': manifest.platform.architecture,
'size': manifest.size,
'digest': manifest.digest,
'description': `${manifest.platform.os} on ${manifest.platform.architecture}`,
};
});
}
return manifest._mapped_manifests;
};
}
})();

View file

@ -0,0 +1,104 @@
(function() {
/**
* Page for creating a new organization.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('new-organization', 'new-organization.html', NewOrgCtrl, {
'newLayout': true,
'title': 'New Organization',
'description': 'Create a new organization to manage teams and permissions'
});
}]);
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features, Config) {
$scope.Features = Features;
$scope.Config = Config
$scope.holder = {};
$scope.org = {
'name': $routeParams['namespace'] || ''
};
UserService.updateUserIn($scope);
var requested = $routeParams['plan'];
if (Features.BILLING) {
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.holder.currentPlan = null;
if (requested) {
PlanService.getPlan(requested, function(plan) {
$scope.holder.currentPlan = plan;
});
}
});
}
$scope.signedIn = function() {
if (Features.BILLING) {
PlanService.handleNotedPlan();
}
};
$scope.signinStarted = function() {
if (Features.BILLING) {
PlanService.getMinimumPlan(1, true, function(plan) {
if (!plan) { return; }
PlanService.notePlan(plan.stripeId);
});
}
};
$scope.setPlan = function(plan) {
$scope.holder.currentPlan = plan;
};
$scope.createNewOrg = function() {
$scope.createError = null;
$scope.creating = true;
var org = $scope.org;
var data = {
'name': org.name,
'email': org.email,
'recaptcha_response': org.recaptcha_response
};
ApiService.createOrganization(data).then(function(created) {
$scope.created = created;
// Reset the organizations list.
UserService.load();
// Set the default namesapce to the organization.
CookieService.putPermanent('quay.namespace', org.name);
var showOrg = function() {
$scope.creating = false;
$location.path('/organization/' + org.name + '/');
};
// If the selected plan is free, simply move to the org page.
if (!Features.BILLING || $scope.holder.currentPlan.price == 0) {
showOrg();
return;
}
// Otherwise, show the subscribe for the plan.
$scope.creating = true;
var callbacks = {
'opened': function() { $scope.creating = true; },
'closed': showOrg,
'success': showOrg,
'failure': showOrg
};
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
}, function(resp) {
$scope.creating = false;
$scope.createError = ApiService.getErrorMessage(resp);
});
};
}
})();

View file

@ -0,0 +1,97 @@
(function() {
/**
* Page to create a new repository.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('new-repo', 'new-repo.html', NewRepoCtrl, {
'newLayout': true,
'title': 'New Repository',
'description': 'Create a new Docker repository'
})
}]);
function NewRepoCtrl($scope, $location, $http, $timeout, $routeParams, UserService, ApiService, PlanService, TriggerService, Features) {
UserService.updateUserIn($scope);
$scope.Features = Features;
$scope.TriggerService = TriggerService;
$scope.repo = {
'is_public': 0,
'description': '',
'initialize': '',
'name': $routeParams['name'],
'repo_kind': 'image'
};
$scope.changeNamespace = function(namespace) {
$scope.repo.namespace = namespace;
};
$scope.$watch('repo.name', function() {
$scope.createError = null;
});
$scope.startBuild = function() {
$scope.buildStarting = true;
$scope.startBuildCallback(function(status, messageOrBuild) {
if (status) {
$location.url('/repository/' + $scope.created.namespace + '/' + $scope.created.name +
'?tab=builds');
} else {
bootbox.alert(messageOrBuild || 'Could not start build');
}
});
};
$scope.readyForBuild = function(startBuild) {
$scope.startBuildCallback = startBuild;
};
$scope.updateDescription = function(content) {
$scope.repo.description = content;
};
$scope.createNewRepo = function() {
$scope.creating = true;
var repo = $scope.repo;
var data = {
'namespace': repo.namespace,
'repository': repo.name,
'visibility': repo.is_public == '1' ? 'public' : 'private',
'description': repo.description,
'repo_kind': repo.repo_kind
};
ApiService.createRepo(data).then(function(created) {
$scope.creating = false;
$scope.created = created;
if (repo.repo_kind == 'application') {
$location.path('/application/' + created.namespace + '/' + created.name);
return;
}
// Start the build if applicable.
if ($scope.repo.initialize == 'dockerfile' || $scope.repo.initialize == 'zipfile') {
$scope.createdForBuild = created;
$scope.startBuild();
return;
}
// Conduct the SCM redirect if applicable.
var redirectUrl = TriggerService.getRedirectUrl($scope.repo.initialize, repo.namespace, repo.name);
if (redirectUrl) {
window.location = redirectUrl;
return;
}
// Otherwise, redirect to the repo page.
$location.path('/repository/' + created.namespace + '/' + created.name);
}, function(result) {
$scope.creating = false;
$scope.createError = ApiService.getErrorMessage(result);
});
};
}
})();

112
static/js/pages/org-view.js Normal file
View file

@ -0,0 +1,112 @@
(function() {
/**
* Page that displays details about an organization, such as its teams.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('org-view', 'org-view.html', OrgViewCtrl, {
'newLayout': true,
'title': 'Organization {{ organization.name }}',
'description': 'Organization {{ organization.name }}'
})
}]);
function OrgViewCtrl($scope, $routeParams, $timeout, ApiService, UIService, AvatarService,
Config, Features, StateService) {
var orgname = $routeParams.orgname;
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
$scope.namespace = orgname;
$scope.showLogsCounter = 0;
$scope.showApplicationsCounter = 0;
$scope.showBillingCounter = 0;
$scope.showRobotsCounter = 0;
$scope.showTeamsCounter = 0;
$scope.changeEmailInfo = null;
$scope.context = {};
$scope.Config = Config;
$scope.Features = Features;
$scope.orgScope = {
'changingOrganization': false,
'organizationEmail': ''
};
$scope.$watch('orgScope.organizationEmail', function(e) {
UIService.hidePopover('#changeEmailForm input');
});
var loadRepositories = function() {
var options = {
'namespace': orgname,
'public': true,
'last_modified': true,
'popularity': true
};
$scope.organization.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories;
});
};
var loadOrganization = function() {
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org;
$scope.orgScope.organizationEmail = org.email;
$scope.isAdmin = org.is_admin;
$scope.isMember = org.is_member;
// Load the repositories.
$timeout(function() {
loadRepositories();
}, 10);
});
};
// Load the organization.
loadOrganization();
$scope.showRobots = function() {
$scope.showRobotsCounter++;
};
$scope.showTeams = function() {
$scope.showTeamsCounter++;
};
$scope.showBilling = function() {
$scope.showBillingCounter = true;
};
$scope.showApplications = function() {
$scope.showApplicationsCounter++;
};
$scope.showLogs = function() {
$scope.showLogsCounter++;
};
$scope.showChangeEmail = function() {
$scope.changeEmailInfo = {
'email': $scope.organization.email
};
};
$scope.changeEmail = function(info, callback) {
var params = {
'orgname': orgname
};
var details = {
'email': $scope.changeEmailInfo.email
};
var errorDisplay = ApiService.errorDisplay('Could not change email address', callback);
ApiService.changeOrganizationDetails(details, params).then(function() {
$scope.organization.email = $scope.changeEmailInfo.email;
callback(true);
}, errorDisplay);
};
}
})();

View file

@ -0,0 +1,16 @@
(function() {
/**
* DEPRECATED: Page which displays the list of organizations of which the user is a member.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('organizations', 'organizations.html', OrgsCtrl, {
'title': 'View Organizations',
'description': 'View and manage your organizations'
});
}]);
function OrgsCtrl($scope, UserService) {
UserService.updateUserIn($scope);
browserchrome.update();
}
})();

13
static/js/pages/plans.js Normal file
View file

@ -0,0 +1,13 @@
(function() {
/**
* The plans/pricing page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('plans', 'plans.html', PlansCtrl, {
'title': 'Plans and Pricing',
'newLayout': true
});
}]);
function PlansCtrl($scope) {}
})();

View file

@ -0,0 +1,10 @@
(function() {
/**
* Privacy page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('privacy', 'privacy.html', null, {
'title': 'Privacy Policy'
});
}]);
}());

View file

@ -0,0 +1,117 @@
(function() {
/**
* Repository listing page. Shows all repositories for all visibile namespaces.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('repo-list', 'repo-list.html', RepoListCtrl, {
'newLayout': true,
'title': 'Repositories',
'description': 'View and manage Docker repositories'
})
}]);
function RepoListCtrl($scope, $sanitize, $q, Restangular, UserService, ApiService, Features,
Config, StateService) {
$scope.namespace = null;
$scope.page = 1;
$scope.publicPageCount = null;
$scope.allRepositories = {};
$scope.loading = true;
$scope.resources = [];
$scope.Features = Features;
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
// When loading the UserService, if the user is logged in, create a list of
// relevant namespaces and collect the relevant repositories.
UserService.updateUserIn($scope, function(user) {
$scope.loading = false;
if (!user.anonymous) {
// Add our user to our list of namespaces.
$scope.namespaces = [{
'name': user.username,
'avatar': user.avatar
}];
// Add each org to our list of namespaces.
user.organizations.map(function(org) {
$scope.namespaces.push({
'name': org.name,
'avatar': org.avatar,
'public': org.public
});
});
// Load the repos.
loadStarredRepos();
loadRepos();
}
});
$scope.isOrganization = function(namespace) {
return !!UserService.getOrganization(namespace);
};
$scope.starToggled = function(repo) {
if (repo.is_starred) {
$scope.starred_repositories.value.push(repo);
} else {
$scope.starred_repositories.value = $scope.starred_repositories.value.filter(function(repo) {
return repo.is_starred;
});
}
};
// Finds a duplicate repo if it exists. If it doesn't, inserts the repo.
var findDuplicateRepo = function(repo) {
var found = $scope.allRepositories[repo.namespace + '/' + repo.name];
if (found) {
return found;
} else {
$scope.allRepositories[repo.namespace + '/' + repo.name] = repo;
return repo;
}
};
var loadStarredRepos = function() {
if (!$scope.user || $scope.user.anonymous) {
return;
}
var options = {
'starred': true,
'last_modified': true,
'popularity': true
};
$scope.starred_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories.map(function(repo) {
repo = findDuplicateRepo(repo);
repo.is_starred = true;
return repo;
});
});
};
var loadRepos = function() {
if (!$scope.user || $scope.user.anonymous || $scope.namespaces.length == 0) {
return;
}
$scope.namespaces.map(function(namespace) {
var options = {
'namespace': namespace.name,
'last_modified': true,
'popularity': true,
'public': namespace.public
};
namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories.map(findDuplicateRepo);
});
$scope.resources.push(namespace.repositories);
});
};
}
})();

View file

@ -0,0 +1,201 @@
(function() {
/**
* Repository view page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('repo-view', 'repo-view.html', RepoViewCtrl, {
'newLayout': true,
'title': '{{ namespace }}/{{ name }}',
'description': 'Repository {{ namespace }}/{{ name }}'
});
}]);
function RepoViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService,
UserService, AngularPollChannel, ImageLoaderService, UtilService) {
$scope.namespace = $routeParams.namespace;
$scope.name = $routeParams.name;
var imageLoader = ImageLoaderService.getLoader($scope.namespace, $scope.name);
// Tab-enabled counters.
$scope.infoShown = 0;
$scope.tagsShown = 0;
$scope.logsShown = 0;
$scope.buildsShown = 0;
$scope.settingsShown = 0;
$scope.historyShown = 0;
$scope.mirrorShown = 0;
$scope.viewScope = {
'selectedTags': [],
'repository': null,
'imageLoader': imageLoader,
'builds': null,
'historyFilter': '',
'repositoryTags': null,
'tagsLoading': true
};
$scope.repositoryTags = {};
var buildPollChannel = null;
// Make sure we track the current user.
UserService.updateUserIn($scope);
// Watch the repository to filter any tags removed.
$scope.$watch('viewScope.repositoryTags', function(repository) {
if (!repository) { return; }
$scope.viewScope.selectedTags = filterTags($scope.viewScope.selectedTags);
});
var filterTags = function(tags) {
return (tags || []).filter(function(tag) {
return !!$scope.viewScope.repositoryTags[tag];
});
};
var loadRepositoryTags = function() {
loadPaginatedRepositoryTags(1);
$scope.viewScope.repositoryTags = $scope.repositoryTags;
};
var loadPaginatedRepositoryTags = function(page) {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'limit': 100,
'page': page,
'onlyActiveTags': true
};
ApiService.listRepoTags(null, params).then(function(resp) {
var newTags = resp.tags.reduce(function(result, item, index, array) {
var tag_name = item['name'];
result[tag_name] = item;
return result;
}, {});
$.extend($scope.repositoryTags, newTags);
if (resp.has_additional) {
loadPaginatedRepositoryTags(page + 1);
} else {
$scope.viewScope.tagsLoading = false;
}
});
};
var loadRepository = function() {
// Mark the images to be reloaded.
$scope.viewScope.images = null;
loadRepositoryTags();
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'includeStats': true,
'includeTags': false
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
if (repo != undefined) {
$scope.repository = repo;
$scope.viewScope.repository = repo;
// Update the page description for SEO
$rootScope.description = UtilService.getFirstMarkdownLineAsString(repo.description);
// Load the remainder of the data async, so we don't block the initial view from showing
$timeout(function() {
$scope.setTags($routeParams.tag);
// Track builds.
buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 30000 /* 30s */);
buildPollChannel.start();
}, 10);
}
});
};
var loadRepositoryBuilds = function(callback) {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'limit': 3
};
var errorHandler = function() {
callback(false);
};
$scope.repositoryBuildsResource = ApiService.getRepoBuildsAsResource(params, /* background */true).get(function(resp) {
// Note: We could just set the builds here, but that causes a full digest cycle. Therefore,
// to be more efficient, we do some work here to determine if anything has changed since
// the last build load in the common case.
if ($scope.viewScope.builds && resp.builds.length == $scope.viewScope.builds.length) {
var hasNewInformation = false;
for (var i = 0; i < resp.builds.length; ++i) {
var current = $scope.viewScope.builds[i];
var updated = resp.builds[i];
if (current.phase != updated.phase || current.id != updated.id) {
hasNewInformation = true;
break;
}
}
if (!hasNewInformation) {
callback(true);
return;
}
}
$scope.viewScope.builds = resp.builds;
callback(true);
}, errorHandler);
};
// Load the repository.
loadRepository();
$scope.setTags = function(tagNames) {
if (!tagNames) {
$scope.viewScope.selectedTags = [];
return;
}
$scope.viewScope.selectedTags = $.unique(tagNames.split(','));
};
$scope.showInfo = function() {
$scope.infoShown++;
};
$scope.showBuilds = function() {
$scope.buildsShown++;
};
$scope.showHistory = function() {
$scope.historyShown++;
};
$scope.showSettings = function() {
$scope.settingsShown++;
};
$scope.showMirror = function() {
$scope.mirrorShown++;
}
$scope.showLogs = function() {
$scope.logsShown++;
};
$scope.showTags = function() {
$timeout(function() {
$scope.tagsShown = 1;
}, 10);
};
$scope.getImages = function(callback) {
loadImages(callback);
};
}
})();

55
static/js/pages/search.js Normal file
View file

@ -0,0 +1,55 @@
(function() {
/**
* Search page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('search', 'search.html', SearchCtrl, {
'title': 'Search'
});
}]);
function SearchCtrl($scope, ApiService, $routeParams, $location, Config) {
var refreshResults = function() {
$scope.currentPage = ($routeParams['page'] || '1') * 1;
var params = {
'query': $routeParams['q'],
'page': $scope.currentPage
};
var MAX_PAGE_RESULTS = Config['SEARCH_MAX_RESULT_PAGE_COUNT'];
var page = $routeParams['page'] || 1;
$scope.maxPopularity = 0;
$scope.resultsResource = ApiService.conductRepoSearchAsResource(params).get(function(resp) {
$scope.results = resp['results'];
// Only show "Next Page" if we have more results, and we aren't on the max page
$scope.showNextButton = page < MAX_PAGE_RESULTS && resp['has_additional'];
// Show some help text if we're on the last page, making them specify the search more
$scope.showMaxResultsHelpText = page >= MAX_PAGE_RESULTS;
$scope.startIndex = resp['start_index'];
resp['results'].forEach(function(result) {
$scope.maxPopularity = Math.max($scope.maxPopularity, result['popularity']);
});
});
};
$scope.previousPage = function() {
$location.search('page', (($routeParams['page'] || 1) * 1) - 1);
};
$scope.nextPage = function() {
$location.search('page', (($routeParams['page'] || 1) * 1) + 1);
};
$scope.currentQuery = $routeParams['q'];
refreshResults();
$scope.$on('$routeUpdate', function(){
$scope.currentQuery = $routeParams['q'];
refreshResults();
});
}
SearchCtrl.$inject = ['$scope', 'ApiService', '$routeParams', '$location', 'Config'];
})();

View file

@ -0,0 +1,10 @@
(function() {
/**
* Security page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('security', 'security.html', null, {
'title': 'Security'
});
}]);
}());

20
static/js/pages/signin.js Normal file
View file

@ -0,0 +1,20 @@
(function() {
/**
* Sign in page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('signin', 'signin.html', SignInCtrl, {
'title': 'Sign In',
});
}]);
function SignInCtrl($scope, $location, ExternalLoginService, Features) {
$scope.redirectUrl = '/';
ExternalLoginService.getSingleSigninUrl(function(singleUrl) {
if (singleUrl) {
document.location = singleUrl;
}
});
}
})();

View file

@ -0,0 +1,190 @@
(function() {
/**
* The superuser admin page provides a new management UI for Red Hat Quay.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('superuser', 'super-user.html', SuperuserCtrl,
{
'newLayout': true,
'title': 'Red Hat Quay Management'
})
}]);
function SuperuserCtrl($scope, $location, ApiService, Features, UserService, ContainerService,
AngularPollChannel, CoreDialog, TableService, StateService) {
if (!Features.SUPER_USERS) {
return;
}
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
$scope.configStatus = null;
$scope.logsCounter = 0;
$scope.changeLog = null;
$scope.logsInstance = null;
$scope.pollChannel = null;
$scope.logsScrolled = false;
$scope.csrf_token = encodeURIComponent(window.__token);
$scope.currentConfig = null;
$scope.serviceKeysActive = false;
$scope.globalMessagesActive = false;
$scope.superUserBuildLogsActive = false;
$scope.manageUsersActive = false;
$scope.orderedOrgs = [];
$scope.orgsPerPage = 10;
$scope.options = {
'predicate': 'name',
'reverse': false,
'filter': null,
'page': 0,
}
$scope.loadMessageOfTheDay = function () {
$scope.globalMessagesActive = true;
};
$scope.loadSuperUserBuildLogs = function () {
$scope.superUserBuildLogsActive = true;
};
$scope.loadServiceKeys = function() {
$scope.serviceKeysActive = true;
};
$scope.getChangeLog = function() {
if ($scope.changeLog) { return; }
ApiService.getChangeLog().then(function(resp) {
$scope.changeLog = resp;
}, ApiService.errorDisplay('Cannot load change log. Please contact support.'))
};
$scope.loadUsageLogs = function() {
$scope.logsCounter++;
};
$scope.loadOrganizations = function() {
if ($scope.organizations) {
return;
}
$scope.loadOrganizationsInternal();
};
var sortOrgs = function() {
if (!$scope.organizations) {return;}
$scope.orderedOrgs = TableService.buildOrderedItems($scope.organizations, $scope.options,
['name', 'email'], []);
};
$scope.loadOrganizationsInternal = function() {
$scope.organizationsResource = ApiService.listAllOrganizationsAsResource().get(function(resp) {
$scope.organizations = resp['organizations'];
sortOrgs();
return $scope.organizations;
});
};
$scope.loadUsers = function() {
$scope.manageUsersActive = true;
};
$scope.tablePredicateClass = function(name, predicate, reverse) {
if (name != predicate) {
return '';
}
return 'current ' + (reverse ? 'reversed' : '');
};
$scope.orderBy = function(predicate) {
if (predicate == $scope.options.predicate) {
$scope.options.reverse = !$scope.options.reverse;
return;
}
$scope.options.reverse = false;
$scope.options.predicate = predicate;
};
$scope.askDeleteOrganization = function(org) {
bootbox.confirm('Are you sure you want to delete this organization? Its data will be deleted with it.',
function(result) {
if (!result) { return; }
var params = {
'name': org.name
};
ApiService.deleteOrganization(null, params).then(function(resp) {
$scope.loadOrganizationsInternal();
}, ApiService.errorDisplay('Could not delete organization'));
});
};
$scope.askRenameOrganization = function(org) {
bootbox.prompt('Enter a new name for the organization:', function(newName) {
if (!newName) { return; }
var params = {
'name': org.name
};
var data = {
'name': newName
};
ApiService.changeOrganization(data, params).then(function(resp) {
$scope.loadOrganizationsInternal();
org.name = newName;
}, ApiService.errorDisplay('Could not rename organization'));
});
};
$scope.askTakeOwnership = function (entity) {
$scope.takeOwnershipInfo = {
'entity': entity
};
};
$scope.takeOwnership = function (info, callback) {
var errorDisplay = ApiService.errorDisplay('Could not take ownership of namespace', callback);
var params = {
'namespace': info.entity.username || info.entity.name
};
ApiService.takeOwnership(null, params).then(function () {
callback(true);
$location.path('/organization/' + params.namespace);
}, errorDisplay)
};
$scope.checkStatus = function() {
ContainerService.checkStatus(function(resp) {
$('#restartingContainerModal').modal('hide');
$scope.configStatus = resp['status'];
$scope.configProviderId = resp['provider_id'];
if ($scope.configStatus == 'ready') {
$scope.currentConfig = null;
$scope.loadUsers();
} else {
var message = "Installation of this product has not yet been completed." +
"<br><br>Please read the " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Setup Guide</a>";
var title = "Installation Incomplete";
CoreDialog.fatal(title, message);
}
}, $scope.currentConfig);
};
// Load the initial status.
$scope.checkStatus();
$scope.$watch('options.predicate', sortOrgs);
$scope.$watch('options.reverse', sortOrgs);
$scope.$watch('options.filter', sortOrgs);
}
}());

View file

@ -0,0 +1,291 @@
(function() {
/**
* Page to view the members of a team and add/remove them.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('team-view', 'team-view.html', TeamViewCtrl, {
'newLayout': true,
'title': 'Team {{ teamname }}',
'description': 'Team {{ teamname }}'
})
}]);
function TeamViewCtrl($rootScope, $scope, $timeout, Features, Restangular, ApiService,
$routeParams, StateService) {
var teamname = $routeParams.teamname;
var orgname = $routeParams.orgname;
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
$scope.context = {};
$scope.orgname = orgname;
$scope.teamname = teamname;
$scope.addingMember = false;
$scope.memberMap = null;
$scope.allowEmail = Features.MAILING;
$scope.feedback = null;
$scope.allowedEntities = ['user', 'robot'];
$rootScope.title = 'Loading...';
$scope.filterFunction = function(invited, robots) {
return function(item) {
// Note: The !! is needed because is_robot will be undefined for invites.
var robot_check = (!!item.is_robot == robots);
return robot_check && item.invited == invited;
};
};
$scope.inviteEmail = function(email) {
if (!email || $scope.memberMap[email]) { return; }
$scope.addingMember = true;
var params = {
'orgname': orgname,
'teamname': teamname,
'email': email
};
var errorHandler = ApiService.errorDisplay('Cannot invite team member', function() {
$scope.addingMember = false;
});
ApiService.inviteTeamMemberEmail(null, params).then(function(resp) {
$scope.members.push(resp);
$scope.memberMap[resp.email] = resp;
$scope.addingMember = false;
$scope.feedback = {
'kind': 'success',
'message': 'E-mail address {email} was invited to join the team',
'data': {
'email': email
}
};
}, errorHandler);
};
$scope.addNewMember = function(member) {
if (!member || $scope.memberMap[member.name]) { return; }
var params = {
'orgname': orgname,
'teamname': teamname,
'membername': member.name
};
var errorHandler = ApiService.errorDisplay('Cannot add team member', function() {
$scope.addingMember = false;
});
$scope.addingMember = true;
ApiService.updateOrganizationTeamMember(null, params).then(function(resp) {
$scope.members.push(resp);
$scope.memberMap[resp.name] = resp;
$scope.addingMember = false;
$scope.feedback = {
'kind': 'success',
'message': 'User {username} was added to the team',
'data': {
'username': member.name
}
};
}, errorHandler);
};
$scope.revokeInvite = function(inviteInfo) {
if (inviteInfo.kind == 'invite') {
// E-mail invite.
$scope.revokeEmailInvite(inviteInfo.email);
} else {
// User invite.
$scope.removeMember(inviteInfo.name);
}
};
$scope.revokeEmailInvite = function(email) {
var params = {
'orgname': orgname,
'teamname': teamname,
'email': email
};
ApiService.deleteTeamMemberEmailInvite(null, params).then(function(resp) {
if (!$scope.memberMap[email]) { return; }
var index = $.inArray($scope.memberMap[email], $scope.members);
$scope.members.splice(index, 1);
delete $scope.memberMap[email];
$scope.feedback = {
'kind': 'success',
'message': 'Invitation to e-amil address {email} was revoked',
'data': {
'email': email
}
};
}, ApiService.errorDisplay('Cannot revoke team invite'));
};
$scope.removeMember = function(username) {
var params = {
'orgname': orgname,
'teamname': teamname,
'membername': username
};
ApiService.deleteOrganizationTeamMember(null, params).then(function(resp) {
if (!$scope.memberMap[username]) { return; }
var index = $.inArray($scope.memberMap[username], $scope.members);
$scope.members.splice(index, 1);
delete $scope.memberMap[username];
$scope.feedback = {
'kind': 'success',
'message': 'User {username} was removed from the team',
'data': {
'username': username
}
};
}, ApiService.errorDisplay('Cannot remove team member'));
};
$scope.getServiceName = function(service) {
switch (service) {
case 'ldap':
return 'LDAP';
case 'keystone':
return 'Keystone Auth';
case 'jwtauthn':
return 'External JWT Auth';
default:
return synced.service;
}
};
$scope.getAddPlaceholder = function(email, synced) {
var kinds = [];
if (!synced) {
kinds.push('registered user');
}
kinds.push('robot');
if (email && !synced) {
kinds.push('email address');
}
kind_string = kinds.join(', ')
return 'Add a ' + kind_string + ' to the team';
};
$scope.updateForDescription = function(content) {
$scope.organization.teams[teamname].description = content;
var params = {
'orgname': orgname,
'teamname': teamname
};
var teaminfo = $scope.organization.teams[teamname];
ApiService.updateOrganizationTeam(teaminfo, params).then(function(resp) {
$scope.feedback = {
'kind': 'success',
'message': 'Team description changed',
'data': {}
};
}, function() {
$('#cannotChangeTeamModal').modal({});
});
};
$scope.showEnableSyncing = function() {
$scope.enableSyncingInfo = {
'service_info': $scope.canSync,
'config': {}
};
};
$scope.showDisableSyncing = function() {
msg = 'Are you sure you want to disable group syncing on this team? ' +
'The team will once again become editable.';
bootbox.confirm(msg, function(result) {
if (result) {
$scope.disableSyncing();
}
});
};
$scope.disableSyncing = function() {
var params = {
'orgname': orgname,
'teamname': teamname
};
var errorHandler = ApiService.errorDisplay('Could not disable team syncing');
ApiService.disableOrganizationTeamSync(null, params).then(function(resp) {
loadMembers();
}, errorHandler);
};
$scope.enableSyncing = function(config, callback) {
var params = {
'orgname': orgname,
'teamname': teamname
};
var errorHandler = ApiService.errorDisplay('Cannot enable team syncing', callback);
ApiService.enableOrganizationTeamSync(config, params).then(function(resp) {
loadMembers();
callback(true);
}, errorHandler);
};
var loadOrganization = function() {
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org;
$scope.team = $scope.organization.teams[teamname];
$rootScope.title = teamname + ' (' + $scope.orgname + ')';
$rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + $scope.orgname;
loadMembers();
return org;
});
};
var loadMembers = function() {
var params = {
'orgname': orgname,
'teamname': teamname,
'includePending': true
};
$scope.membersResource = ApiService.getOrganizationTeamMembersAsResource(params).get(function(resp) {
$scope.members = resp.members;
$scope.canEditMembers = resp.can_edit;
$scope.canSync = resp.can_sync;
$scope.syncInfo = resp.synced;
$scope.allowedEntities = resp.synced ? ['robot'] : ['user', 'robot'];
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
$scope.memberMap = {};
for (var i = 0; i < $scope.members.length; ++i) {
var current = $scope.members[i];
$scope.memberMap[current.name || current.email] = current;
}
return resp.members;
});
};
// Load the organization.
loadOrganization();
}
})();

10
static/js/pages/tos.js Normal file
View file

@ -0,0 +1,10 @@
(function() {
/**
* TOS page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('tos', 'tos.html', null, {
'title': 'Terms of Service'
});
}]);
}());

15
static/js/pages/tour.js Normal file
View file

@ -0,0 +1,15 @@
(function() {
/**
* The site tour page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('tour', 'tour.html', TourCtrl, {
'title': 'Feature Tour',
'description': 'Take a tour of Quay\'s features'
});
}]);
function TourCtrl($scope, $location) {
$scope.kind = $location.path().substring('/tour/'.length);
}
})();

View file

@ -0,0 +1,90 @@
(function() {
/**
* Trigger setup page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('trigger-setup', 'trigger-setup.html', TriggerSetupCtrl, {
'title': 'Setup build trigger',
'description': 'Setup build trigger',
'newLayout': true
});
}]);
function TriggerSetupCtrl($scope, ApiService, $routeParams, $location, UserService, TriggerService) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var trigger_uuid = $routeParams.triggerid;
var loadRepository = function() {
var params = {
'repository': namespace + '/' + name,
'includeTags': false
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repository = repo;
});
};
var loadTrigger = function() {
var params = {
'repository': namespace + '/' + name,
'trigger_uuid': trigger_uuid
};
$scope.triggerResource = ApiService.getBuildTriggerAsResource(params).get(function(trigger) {
$scope.trigger = trigger;
});
};
loadTrigger();
loadRepository();
$scope.state = 'managing';
$scope.activateTrigger = function(event) {
$scope.state = 'activating';
var params = {
'repository': namespace + '/' + name,
'trigger_uuid': trigger_uuid
};
var data = {
'config': event.config
};
if (event.pull_robot) {
data['pull_robot'] = event.pull_robot['name'];
}
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
$scope.state = 'managing';
return ApiService.getErrorMessage(resp) +
'\n\nNote: Errors can occur if you do not have admin access on the repository';
});
ApiService.activateBuildTrigger(data, params).then(function(resp) {
$scope.trigger['is_active'] = true;
$scope.trigger['config'] = resp['config'];
$scope.trigger['pull_robot'] = resp['pull_robot'];
$scope.trigger['repository_url'] = resp['repository_url'];
$scope.state = 'activated';
// If there are no credentials to display, redirect to the builds tab.
if (!$scope.trigger['config'].credentials) {
$location.url('/repository/' + namespace + '/' + name + '?tab=builds');
}
}, errorHandler);
};
$scope.getTriggerIcon = function() {
if (!$scope.trigger) { return ''; }
return TriggerService.getIcon($scope.trigger.service);
};
$scope.getTriggerId = function() {
if (!trigger_uuid) { return ''; }
return trigger_uuid.split('-')[0];
};
}
}());

200
static/js/pages/tutorial.js Normal file
View file

@ -0,0 +1,200 @@
(function() {
/**
* Interactive tutorial page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('tutorial', 'tutorial.html', TutorialCtrl, {
'newLayout': true,
'title': 'Tutorial',
'description': 'Basic tutorial on using Quay'
})
}]);
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Config, Features) {
// Default to showing sudo on all commands if on linux.
var showSudo = navigator.appVersion.indexOf("Linux") != -1;
$scope.tour = {
'title': Config.REGISTRY_TITLE_SHORT + ' Tutorial',
'initialScope': {
'showSudo': showSudo,
'domainName': Config.getDomain()
},
'steps': [
{
'title': 'Welcome to the ' + Config.REGISTRY_TITLE_SHORT + ' tutorial!',
'templateUrl': '/static/tutorial/welcome.html'
},
{
'title': 'Sign in to get started',
'templateUrl': '/static/tutorial/signup.html',
'signal': function($tourScope) {
var user = UserService.currentUser();
$tourScope.username = user.username;
$tourScope.email = user.email;
$tourScope.inOrganization = user.organizations && user.organizations.length > 0;
return !user.anonymous;
}
},
{
'title': 'Step 1: Login to ' + Config.REGISTRY_TITLE_SHORT,
'templateUrl': '/static/tutorial/docker-login.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message) {
return message['data']['action'] == 'login';
}),
'waitMessage': "Waiting for docker login",
'skipTitle': "I'm already logged in",
'mixpanelEvent': 'tutorial_start'
},
{
'title': 'Step 2: Create a new container',
'templateUrl': '/static/tutorial/create-container.html'
},
{
'title': 'Step 3: Create a new image',
'templateUrl': '/static/tutorial/create-image.html'
},
{
'title': 'Step 4: Push the image to ' + Config.REGISTRY_TITLE_SHORT,
'templateUrl': '/static/tutorial/push-image.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message, tourScope) {
var pushing = message['data']['action'] == 'push_start';
if (pushing) {
tourScope.repoName = message['data']['repository'];
}
return pushing;
}),
'waitMessage': "Waiting for repository push to begin",
'mixpanelEvent': 'tutorial_wait_for_push'
},
{
'title': 'Push in progress',
'templateUrl': '/static/tutorial/pushing.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message, tourScope) {
return message['data']['action'] == 'push_repo';
}),
'waitMessage': "Waiting for repository push to complete"
},
{
'title': 'Step 5: View the repository on ' + Config.REGISTRY_TITLE_SHORT,
'templateUrl': '/static/tutorial/view-repo.html',
'signal': AngularTourSignals.matchesLocation('/repository/'),
'overlayable': true,
'mixpanelEvent': 'tutorial_push_complete'
},
{
'templateUrl': '/static/tutorial/view-repo.html',
'signal': AngularTourSignals.matchesLocation('/repository/'),
'overlayable': true
},
{
'templateUrl': '/static/tutorial/waiting-repo-list.html',
'signal': AngularTourSignals.elementAvaliable('*[data-repo="{{username}}/{{repoName}}"]'),
'overlayable': true
},
{
'templateUrl': '/static/tutorial/repo-list.html',
'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}'),
'element': '*[data-repo="{{username}}/{{repoName}}"]',
'overlayable': true
},
{
'title': 'Repository View',
'content': 'This is the repository view page. It displays all the primary information about your repository',
'overlayable': true,
'mixpanelEvent': 'tutorial_view_repo'
},
{
'title': 'Repository Tags',
'content': 'Click on the tags tab to view all the tags in the repository',
'overlayable': true,
'element': '#tagsTab',
'signal': AngularTourSignals.elementVisible('*[id="tagsTable"]')
},
{
'title': 'Tag List',
'content': 'The tag list displays shows the full list of active tags in the repository. ' +
'You can click on an image to see its information or click on a tag to see its history.',
'element': '#tagsTable',
'overlayable': true
},
{
'title': 'Tag Information',
'content': 'Each row displays information about a specific tag',
'element': '#tagsTable tr:first-child',
'overlayable': true
},
{
'title': 'Tag Actions',
'content': 'You can modify a tag by clicking on the Tag Options icon',
'element': '#tagsTable tr:first-child .fa-gear',
'overlayable': true
},
{
'title': 'Tag History',
'content': 'You can view a tags history by clicking on the Tag History icon',
'element': '#tagsTable tr:first-child .fa-history',
'overlayable': true
},
{
'title': 'Fetch Tag',
'content': 'To see the various ways to fetch/pull a tag, click the Fetch Tag icon',
'element': '#tagsTable tr:first-child .fa-download',
'overlayable': true
},
{
'content': 'To view the permissions for a repository, click on the Gear tab',
'element': '#settingsTab',
'overlayable': true,
'signal': AngularTourSignals.elementVisible('*[id="repoPermissions"]')
},
{
'title': 'Repository Settings',
'content': "The repository settings tab allows for modification of a repository's permissions, notifications, visibility and other settings",
'overlayable': true,
'mixpanelEvent': 'tutorial_view_admin'
},
{
'title': 'Permissions',
'templateUrl': '/static/tutorial/permissions.html',
'overlayable': true,
'element': '#repoPermissions'
},
{
'title': 'Adding a permission',
'content': 'To add an <b>additional</b> permission, enter a username or robot account name into the autocomplete ' +
'or hit the dropdown arrow to manage robot accounts',
'overlayable': true,
'element': '#add-entity-permission'
},
{
'content': 'Repositories can be automatically populated in response to a Dockerfile build. To view the build settings for a repository, click on the builds tab',
'element': '#buildsTab',
'overlayable': true,
'signal': AngularTourSignals.elementVisible('*[id="repoBuilds"]'),
'skip': !Features.BUILD_SUPPORT
},
{
'content': 'New build triggers can be created by clicking the "Create Build Trigger" button.',
'element': '#addBuildTrigger',
'overlayable': true,
'skip': !Features.BUILD_SUPPORT
},
{
'content': 'The full build history can always be referenced and filtered in the builds list.',
'element': '#repoBuilds',
'overlayable': true,
'skip': !Features.BUILD_SUPPORT
},
{
'templateUrl': '/static/tutorial/done.html',
'overlayable': true,
'mixpanelEvent': 'tutorial_complete'
}
]
};
}
})();

View file

@ -0,0 +1,97 @@
(function() {
/**
* Update user page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('update-user', 'update-user.html', UpdateUserCtrl, {
'title': 'Confirm Username'
});
}]);
function UpdateUserCtrl($scope, UserService, $location, ApiService) {
$scope.state = 'loading';
$scope.metadata = {};
UserService.updateUserIn($scope, function(user) {
if (!user.anonymous) {
if (!user.prompts || !user.prompts.length) {
$location.path('/');
return;
}
$scope.state = 'editing';
$scope.username = user.username;
}
});
var confirmUsername = function(username) {
if (username == $scope.user.username) {
$scope.state = 'confirmed';
return;
}
$scope.state = 'confirming';
var params = {
'username': username
};
var oparams = {
'orgname': username
};
ApiService.getUserInformation(null, params).then(function() {
$scope.state = 'existing';
}, function(resp) {
ApiService.getOrganization(null, oparams).then(function() {
$scope.state = 'existing';
}, function() {
if (resp.status == 404) {
$scope.state = 'confirmed';
} else {
$scope.state = 'error';
}
});
});
};
$scope.updateUser = function(data) {
$scope.state = 'updating';
var errorHandler = ApiService.errorDisplay('Could not update user information', function() {
$scope.state = 'editing';
});
ApiService.changeUserDetails(data).then(function() {
UserService.load(function(updated) {
if (updated && updated.prompts && updated.prompts.length) {
$scope.state = 'editing';
} else {
$location.url('/');
}
});
}, errorHandler);
};
$scope.hasPrompt = function(user, prompt_name) {
if (!user || !user.prompts) {
return false;
}
for (var i = 0; i < user.prompts.length; ++i) {
if (user.prompts[i] == prompt_name) {
return true;
}
}
return false;
};
$scope.$watch('username', function(username) {
if (!username) {
$scope.state = 'editing';
return;
}
confirmUsername(username);
});
}
})();

View file

@ -0,0 +1,253 @@
(function() {
/**
* Page that displays details about an user.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('user-view', 'user-view.html', UserViewCtrl, {
'newLayout': true,
'title': 'User {{ user.username }}',
'description': 'User {{ user.username }}'
})
}]);
function UserViewCtrl($scope, $routeParams, $timeout, ApiService, UserService, UIService,
AvatarService, Config, ExternalLoginService, CookieService, StateService) {
var username = $routeParams.username;
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
$scope.Config = Config;
$scope.showAppsCounter = 0;
$scope.showRobotsCounter = 0;
$scope.showBillingCounter = 0;
$scope.showLogsCounter = 0;
$scope.changeEmailInfo = null;
$scope.changePasswordInfo = null;
$scope.changeMetadataInfo = null;
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
$scope.context = {};
$scope.oidcLoginProvider = null;
if (Config['INTERNAL_OIDC_SERVICE_ID']) {
ExternalLoginService.EXTERNAL_LOGINS.forEach(function(provider) {
if (provider.id == Config['INTERNAL_OIDC_SERVICE_ID']) {
$scope.oidcLoginProvider = provider;
}
});
}
UserService.updateUserIn($scope, function(user) {
if (user && user.username) {
if ($scope.oidcLoginProvider && $routeParams['idtoken']) {
$scope.context.idTokenCredentials = {
'username': UserService.getCLIUsername(),
'password': $routeParams['idtoken'],
'namespace': UserService.currentUser().username
};
}
}
});
var loadRepositories = function() {
var options = {
'public': true,
'namespace': username,
'last_modified': true,
'popularity': true
};
$scope.context.viewuser.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories;
});
};
var loadUser = function() {
$scope.userResource = ApiService.getUserInformationAsResource({'username': username}).get(function(user) {
$scope.context.viewuser = user;
$scope.viewuser = user;
$timeout(function() {
// Load the repositories.
loadRepositories();
// Show the password change dialog if immediately after an account recovery.
if ($routeParams.action == 'password' && UserService.isNamespaceAdmin(username)) {
$scope.showChangePassword();
}
}, 10);
});
};
// Load the user.
loadUser();
$scope.showRobots = function() {
$scope.showRobotsCounter++;
};
$scope.showLogs = function() {
$scope.showLogsCounter++;
};
$scope.showApplications = function() {
$scope.showAppsCounter++;
};
$scope.showChangePassword = function() {
$scope.changePasswordInfo = {};
};
$scope.changePassword = function(info, callback) {
if (Config.AUTHENTICATION_TYPE != 'Database') { return; }
var data = {
'password': $scope.changePasswordInfo.password
};
var errorDisplay = ApiService.errorDisplay('Could not change password', callback);
ApiService.changeUserDetails(data).then(function(resp) {
// Reload the user.
UserService.load();
callback(true);
}, errorDisplay);
};
$scope.generateClientToken = function() {
var generateToken = function(password) {
if (!password) {
return;
}
var data = {
'password': password
};
ApiService.generateUserClientKey(data).then(function(resp) {
$scope.context.encryptedPasswordCredentials = {
'username': UserService.getCLIUsername(),
'password': resp['key'],
'namespace': UserService.currentUser().username
};
}, ApiService.errorDisplay('Could not generate token'));
};
UIService.showPasswordDialog('Enter your password to generate an encrypted version:', generateToken);
};
$scope.showChangeMetadata = function(field_name, field_title) {
$scope.changeMetadataInfo = {
'value': $scope.context.viewuser[field_name],
'field': field_name,
'title': field_title
};
};
$scope.updateMetadataInfo = function(info, callback) {
var details = {};
details[info.field] = (info.value === '' ? null : info.value);
var errorDisplay = ApiService.errorDisplay('Could not update ' + info.title, callback);
ApiService.changeUserDetails(details).then(function() {
$scope.context.viewuser[info.field] = info.value;
callback(true);
}, errorDisplay);
};
$scope.showChangeEmail = function() {
$scope.changeEmailInfo = {
'email': $scope.context.viewuser.email
};
};
$scope.changeEmail = function(info, callback) {
var details = {
'email': $scope.changeEmailInfo.email
};
var errorDisplay = ApiService.errorDisplay('Could not change email address', callback);
ApiService.changeUserDetails(details).then(function() {
$scope.context.emailAwaitingChange = $scope.changeEmailInfo.email;
callback(true);
}, errorDisplay);
};
$scope.showChangeAccount = function() {
$scope.convertAccountInfo = {
'user': $scope.context.viewuser
};
};
$scope.showBilling = function() {
$scope.showBillingCounter++;
};
$scope.notificationsPermissionsEnabled = window['Notification']
&& Notification.permission === 'granted'
&& CookieService.get('quay.enabledDesktopNotifications') === 'on';
$scope.desktopNotificationsPermissionIsDisabled = () => window['Notification'] && Notification.permission === 'denied';
$scope.toggleDesktopNotifications = () => {
if (!window['Notification']) { // unsupported in IE & some older browsers, we'll just tell the user it's not available
bootbox.dialog({
"message": 'Desktop Notifications unsupported in this browser',
"title": 'Unsupported Option',
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
return;
}
if (CookieService.get('quay.enabledDesktopNotifications') === 'on') {
bootbox.confirm('Are you sure you want to turn off browser notifications?', confirmed => {
if (confirmed) {
CookieService.putPermanent('quay.enabledDesktopNotifications', 'off');
CookieService.clear('quay.notifications.mostRecentTimestamp');
$scope.$apply(() => {
$scope.notificationsPermissionsEnabled = false;
});
}
});
} else {
if (Notification.permission === 'default') {
Notification.requestPermission()
.then((newPermission) => {
if (newPermission === 'granted') {
CookieService.putPermanent('quay.enabledDesktopNotifications', 'on');
CookieService.putPermanent('quay.notifications.mostRecentTimestamp', new Date().getTime().toString());
}
$scope.$apply(() => {
$scope.notificationsPermissionsEnabled = (newPermission === 'granted');
});
});
} else if (Notification.permission === 'granted') {
bootbox.confirm('Are you sure you want to turn on browser notifications?', confirmed => {
if (confirmed) {
CookieService.putPermanent('quay.enabledDesktopNotifications', 'on');
CookieService.putPermanent('quay.notifications.mostRecentTimestamp', new Date().getTime().toString());
$scope.$apply(() => {
$scope.notificationsPermissionsEnabled = true;
});
}
});
}
}
};
}
})();