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

View file

@ -0,0 +1,281 @@
/**
* An element which displays the builds panel for a repository view.
*/
angular.module('quay').directive('repoPanelBuilds', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-view/repo-panel-builds.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'builds': '=builds',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, $filter, $routeParams, ApiService, TriggerService, UserService, StateService) {
StateService.updateStateIn($scope, function(state) {
$scope.inReadOnlyMode = state.inReadOnlyMode;
});
var orderBy = $filter('orderBy');
$scope.TriggerService = TriggerService;
UserService.updateUserIn($scope);
$scope.options = {
'filter': 'recent',
'reverse': false,
'predicate': 'started_datetime'
};
$scope.currentFilter = null;
$scope.currentStartTrigger = null;
$scope.showBuildDialogCounter = 0;
$scope.showTriggerStartDialogCounter = 0;
$scope.triggerCredentialsModalTrigger = null;
$scope.triggerCredentialsModalCounter = 0;
$scope.feedback = null;
var updateBuilds = function() {
if (!$scope.allBuilds) { return; }
var unordered = $scope.allBuilds.map(function(build_info) {
var commit_sha = null;
if (build_info.trigger_metadata) {
commit_sha = TriggerService.getCommitSHA(build_info.trigger_metadata);
}
return $.extend(build_info, {
'started_datetime': (new Date(build_info.started)).valueOf() * (-1),
'building_tags': build_info.tags || [],
'commit_sha': commit_sha
});
});
$scope.fullBuilds = orderBy(unordered, $scope.options.predicate, $scope.options.reverse);
};
var loadBuilds = function(opt_forcerefresh) {
if (!$scope.builds || !$scope.repository || !$scope.options.filter || !$scope.isEnabled) {
return;
}
// Note: We only refresh if the filter has changed.
var filter = $scope.options.filter;
if ($scope.buildsResource && filter == $scope.currentFilter && !opt_forcerefresh) {
return;
}
var since = null;
var limit = 10;
if ($scope.options.filter == '48hour') {
since = Math.floor(moment().subtract(2, 'days').valueOf() / 1000);
limit = 100;
} else if ($scope.options.filter == '30day') {
since = Math.floor(moment().subtract(30, 'days').valueOf() / 1000);
limit = 100;
} else {
since = null;
limit = 10;
}
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'limit': limit,
'since': since
};
$scope.buildsResource = ApiService.getRepoBuildsAsResource(params).get(function(resp) {
$scope.allBuilds = resp.builds;
$scope.currentFilter = filter;
updateBuilds();
});
};
var buildsChanged = function() {
if (!$scope.allBuilds) {
loadBuilds();
return;
}
if (!$scope.builds || !$scope.repository || !$scope.isEnabled) {
return;
}
// Replace any build records with updated records from the server.
var requireReload = false;
$scope.builds.map(function(build) {
var found = false;
for (var i = 0; i < $scope.allBuilds.length; ++i) {
var current = $scope.allBuilds[i];
if (current.id == build.id && current.phase != build.phase) {
$scope.allBuilds[i] = build;
found = true;
break;
}
}
// If the build was not found, then a new build has started. Reload
// the builds list.
if (!found) {
requireReload = true;
}
});
if (requireReload) {
loadBuilds(/* force refresh */true);
} else {
updateBuilds();
}
};
var loadBuildTriggers = function() {
if (!$scope.repository || !$scope.repository.can_admin || !$scope.isEnabled) { return; }
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name
};
$scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) {
$scope.triggers = resp.triggers;
});
};
$scope.$watch('repository', loadBuildTriggers);
$scope.$watch('repository', loadBuilds);
$scope.$watch('isEnabled', loadBuildTriggers);
$scope.$watch('isEnabled', loadBuilds);
$scope.$watch('builds', buildsChanged);
$scope.$watch('options.filter', loadBuilds);
$scope.$watch('options.predicate', updateBuilds);
$scope.$watch('options.reverse', updateBuilds);
$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.showTriggerCredentialsModal = function(trigger) {
$scope.triggerCredentialsModalTrigger = trigger;
$scope.triggerCredentialsModalCounter++;
};
$scope.askDeleteTrigger = function(trigger) {
$scope.deleteTriggerInfo = {
'trigger': trigger
};
};
$scope.askRunTrigger = function(trigger) {
if (!trigger.enabled) {
return;
}
if (!trigger.can_invoke) {
bootbox.alert('You do not have permission to manually invoke this trigger');
return;
}
$scope.currentStartTrigger = trigger;
$scope.showTriggerStartDialogCounter++;
};
$scope.askToggleTrigger = function(trigger) {
if (!trigger.can_invoke) {
bootbox.alert('You do not have permission to edit this trigger');
return;
}
$scope.toggleTriggerInfo = {
'trigger': trigger
};
};
$scope.toggleTrigger = function(trigger, opt_callback) {
if (!trigger) { return; }
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': trigger.id
};
var data = {
'enabled': !trigger.enabled
};
var errorHandler = ApiService.errorDisplay('Could not toggle build trigger', function() {
opt_callback && opt_callback(false);
});
ApiService.updateBuildTrigger(data, params).then(function(resp) {
trigger.enabled = !trigger.enabled;
trigger.disabled_reason = 'user_toggled';
opt_callback && opt_callback(true);
}, errorHandler);
};
$scope.deleteTrigger = function(trigger, opt_callback) {
if (!trigger) { return; }
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': trigger.id
};
var errorHandler = ApiService.errorDisplay('Could not delete build trigger', function() {
opt_callback && opt_callback(false);
});
ApiService.deleteBuildTrigger(null, params).then(function(resp) {
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
opt_callback && opt_callback(true);
}, errorHandler);
};
$scope.showNewBuildDialog = function() {
$scope.showBuildDialogCounter++;
};
$scope.handleBuildStarted = function(build) {
if ($scope.allBuilds) {
$scope.allBuilds.push(build);
}
updateBuilds();
$scope.feedback = {
'kind': 'info',
'message': 'Build {buildid} started',
'data': {
'buildid': build.id
}
};
};
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,58 @@
/**
* An element which displays the information panel for a repository view.
*/
angular.module('quay').directive('repoPanelInfo', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-view/repo-panel-info.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'builds': '=builds',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, ApiService, Config, Features, StateService) {
StateService.updateStateIn($scope, function(state) {
$scope.inReadOnlyMode = state.inReadOnlyMode;
});
$scope.Features = Features;
$scope.$watch('repository', function(repository) {
if (!$scope.repository) { return; }
var namespace = $scope.repository.namespace;
var name = $scope.repository.name;
$scope.pullCommand = 'docker pull ' + Config.getDomain() + '/' + namespace + '/' + name;
});
$scope.updateDescription = function(content) {
$scope.repository.description = content;
$scope.repository.put();
};
$scope.getAggregatedUsage = function(stats, days) {
if (!stats || !stats.length) {
return 0;
}
var count = 0;
var startDate = moment().subtract(days + 1, 'days');
for (var i = 0; i < stats.length; ++i) {
var stat = stats[i];
var statDate = moment(stat['date']);
if (statDate.isBefore(startDate)) {
continue;
}
count += stat['count'];
}
return count;
};
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,468 @@
import { vendor } from "postcss";
/**
* An element which displays the mirroring panel for a repository view.
*/
angular.module('quay').directive('repoPanelMirror', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-view/repo-panel-mirroring.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'isEnabled': '=isEnabled'
},
controllerAs: 'vm',
controller: function ($scope, ApiService, Features) {
let vm = this;
// Feature Flagged
if (!Features.REPO_MIRROR) { return; }
// Shared by API Calls
let params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name };
/**
* Mirror Configuration
*/
vm.isSetup = false;
vm.expirationDate = null;
vm.isEnabled = null;
vm.httpProxy = null;
vm.httpsProxy = null;
vm.externalReference = null;
vm.noProxy = null;
vm.retriesRemaining = null;
vm.robot = null;
vm.status = null;
vm.syncInterval = null;
vm.syncStartDate = null;
vm.tags = null;
vm.username = null;
vm.verifyTLS = null;
/**
* Fetch the latest Repository Mirror Configuration
*/
vm.getMirror = function() {
ApiService.getRepoMirrorConfig(null, params)
.then(function (resp) {
vm.isSetup = true;
vm.isEnabled = resp.is_enabled;
vm.externalReference = resp.external_reference;
vm.syncInterval = resp.sync_interval;
vm.username = resp.external_registry_username;
vm.syncStartDate = resp.sync_start_date;
vm.status = resp.sync_status;
vm.expirationDate = resp.sync_expiration_date;
vm.retriesRemaining = resp.sync_retries_remaining;
vm.robot = {};
if (resp.robot_username) {
vm.robot = {
'name': resp.robot_username,
'kind': 'user',
'is_robot': true
};
}
vm.tags = resp.root_rule.rule_value || []; // TODO: Use RepoMirrorRule-specific endpoint
// TODO: These are not consistently provided by the API. Correct that in the API.
vm.verifyTLS = resp.external_registry_config.verify_tls;
if (resp.external_registry_config.proxy) {
vm.httpProxy = resp.external_registry_config.proxy.http_proxy;
vm.httpsProxy = resp.external_registry_config.proxy.https_proxy;
vm.noProxy = resp.external_registry_config.proxy.no_proxy;
}
}, function (err) { console.info("No repository mirror configuration.", err); });
}
/**
* Human-friendly status messages
*/
vm.statusLabels = {
"NEVER_RUN": "Scheduled",
"SYNC_NOW": "Scheduled Now",
"SYNCING": "Sync In Progress",
"SUCCESS": "Last Sync Succeeded",
"FAIL": "Last Sync Failed"
}
/**
* Convert (Unix) Timestamp to ISO Formatted Date String used by the API
*/
vm.timestampToISO = function(ts) {
let dt = moment.unix(ts).toISOString().split('.')[0] + 'Z'; // Remove milliseconds
return dt;
}
/**
* Convert ISO Date String to (Unix) Timestamp
*/
vm.timestampFromISO = function(dt) {
let ts = moment(dt).unix();
return ts;
}
/**
* When set to a truthy value, any `cor-confirm-dialog` associated with these variables will
* be displayed.
*/
vm.credentialsChanges = null;
vm.httpProxyChanges = null;
vm.httpsProxyChanges = null;
vm.locationChanges = null;
vm.noProxyChanges = null;
vm.syncIntervalChanges = null;
vm.syncStartDateChanges = null;
vm.tagChanges = null;
/**
* The following `show` functions initialize and trigger the display of a modal to modify
* configuration attributes.
*/
vm.showChangeSyncInterval = function() {
vm.syncIntervalChanges = {
'fieldName': 'synchronization interval',
'values': {
'sync_interval': vm.syncInterval
}
}
}
vm.showChangeSyncStartDate = function() {
let ts = vm.timestampFromISO(vm.syncStartDate);
vm.syncStartDateChanges = {
'fieldName': 'next synchronization date',
'values': {
'sync_start_date': ts
}
}
}
vm.showChangeTags = function() {
vm.tagChanges = {
'fieldName': 'tag patterns',
'values': {
'rule_value': vm.tags || []
}
}
}
vm.showChangeCredentials = function() {
vm.credentialsChanges = {
'fieldName': 'credentials',
'values': {
'external_registry_username': vm.username,
'external_registry_password': null
}
}
}
vm.showChangeHTTPProxy = function() {
vm.httpProxyChanges = {
'fieldName': 'HTTP Proxy',
'values': {
'external_registry_config': {
'proxy': {
'http_proxy': vm.httpProxy
}
}
}
}
}
vm.showChangeHTTPsProxy = function() {
vm.httpsProxyChanges = {
'fieldName': 'HTTPs Proxy',
'values': {
'external_registry_config': {
'proxy': {
'https_proxy': vm.httpsProxy
}
}
}
}
}
vm.showChangeNoProxy = function() {
vm.noProxyChanges = {
'fieldName': 'No Proxy',
'values': {
'external_registry_config': {
'proxy': {
'no_proxy': vm.noProxy
}
}
}
}
}
vm.showChangeExternalRepository = function() {
vm.externalRepositoryChanges = {
'fieldName': 'External Repository',
'values': {
'external_reference': vm.externalReference
}
}
}
/**
* Submit API request to modify repository mirroring attribute(s)
*/
vm.changeConfig = function(data, callback) {
let fieldName = data.fieldName || 'configuration';
let requestBody = data.values;
let errMsg = "Unable to change " + fieldName + '.';
let handleError = ApiService.errorDisplay(errMsg, callback);
let handleSuccess = function() {
vm.getMirror(); // Fetch updated configuration
if (callback) callback(true);
}
ApiService.changeRepoMirrorConfig(requestBody, params)
.then(handleSuccess, handleError);
return true;
}
/**
* Transform the DatePicker's Unix timestamp into a string compatible with the API
* before attempting to change it.
*/
vm.changeSyncStartDate = function(data, callback) {
let newSyncStartDate = vm.timestampToISO(data.values.sync_start_date);
data.values.sync_start_date = newSyncStartDate;
return vm.changeConfig(data, callback);
}
/**
* Enable `Verify TLS`.
*/
vm.enableVerifyTLS = function() {
let data = {
'fieldName': 'TLS verification',
'values': {
'external_registry_config': {
'verify_tls': true
}
}
}
return vm.changeConfig(data, null);
}
/**
* Disable `Verify TLS`
*/
vm.disableVerifyTLS = function() {
let data = {
'fieldName': 'TLS verification',
'values': {
'external_registry_config': {
'verify_tls': false
}
}
}
return vm.changeConfig(data, null);
}
/**
* Toggle `Verify TLS`.
*/
vm.toggleVerifyTLS = function() {
if (vm.verifyTLS) return vm.disableVerifyTLS();
else return vm.enableVerifyTLS();
}
/**
* Change Robot user.
*/
vm.changeRobot = function(robot) {
if (!vm.robot) return;
if (!robot || robot.name == vm.robot.name) return;
let data = {
'fieldName': 'robot',
'values': {
'robot_username': robot.name
}
}
return vm.changeConfig(data, null)
}
/**
* Delete Credentials
*/
vm.deleteCredentials = function() {
let data = {
'fieldName': 'credentials',
'values': {
'external_registry_username': null,
'external_registry_password': null
}
}
return vm.changeConfig(data, null);
}
/**
* Enable mirroring configuration.
*/
vm.enableMirroring = function() {
let data = {
'fieldName': 'enabled state',
'values': {
'is_enabled': true
}
}
return vm.changeConfig(data, null)
}
/**
* Disable mirroring configuration.
*/
vm.disableMirroring = function() {
let data = {
'fieldName': 'enabled state',
'values': {
'is_enabled': false
}
}
return vm.changeConfig(data, null)
}
/**
* Toggle mirroring on/off.
*/
vm.toggleMirroring = function() {
if (vm.isEnabled) return vm.disableMirroring();
else return vm.enableMirroring();
}
/**
* Update Tag-Rules
*/
vm.changeTagRules = function(data, callback) {
let csv = data.values.rule_value;
let patterns = csv.split(',');
patterns.map(s => s.trim()); // Trim excess whitespace
patterns = Array.from(new Set(patterns)); // De-duplicate
if (patterns.length < 1) {
bootbox.alert('Rule value required');
callback(false);
return false;
}
data = {
'root_rule': {
'rule_type': 'TAG_GLOB_CSV',
'rule_value': patterns
}
}
let displayError = ApiService.errorDisplay('Could not change Tag Rules', callback);
ApiService
.changeRepoMirrorRule(data, params)
.then(function(resp) {
vm.getMirror();
callback(true);
}, displayError);
return true;
}
/**
* Trigger Immediate Synchronization
*/
vm.syncNow = function () {
let displayError = ApiService.errorDisplay('Unable to sync now', null);
ApiService
.syncNow(null, params)
.then(function(resp) {
vm.getMirror(); // Reload latest changes
return true;
}, displayError);
return true;
}
/**
* Cancel In-Progress Synchronization
*/
vm.syncCancel = function() {
let displayError = ApiService.errorDisplay('Unable to cancel sync', null);
ApiService
.syncCancel(null, params)
.then(function(resp) {
vm.getMirror(); // Reload latest changes
return true;
}, displayError);
return true;
}
// Load the current mirror configuration on initialization
if ($scope.repository.state == 'MIRROR') {
vm.getMirror();
}
/**
* Configure mirroing.
* TODO: Move this, and the associated template/view, to its own component and use the
* wizard-flow instead of a single form.
*/
vm.setupMirror = function() {
// Apply transformations
let now = vm.timestampToISO(moment().unix());
let syncStartDate = vm.timestampToISO(vm.syncStartDate) || now;
let patterns = Array.from(new Set(vm.tags.split(',').map(s => s.trim()))); // trim + de-dupe
let requestBody = {
'external_reference': vm.externalReference,
'external_registry_username': vm.username,
'external_registry_password': vm.password,
'sync_interval': vm.syncInterval,
'sync_start_date': syncStartDate,
'robot_username': vm.robot.name,
'external_registry_config': {
'verify_tls': vm.verifyTLS || false, // `null` not allowed
'proxy': {
'http_proxy': vm.httpProxy,
'https_proxy': vm.httpsProxy,
'no_proxy': vm.noProxy
}
},
'root_rule': {
'rule_type': 'TAG_GLOB_CSV',
'rule_value': patterns
}
}
let successHandler = function(resp) { vm.getMirror(); return true; }
let errorHandler = ApiService.errorDisplay('Unable to setup mirror.', null);
ApiService.createRepoMirrorConfig(requestBody, params).then(successHandler, errorHandler);
}
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,123 @@
/**
* An element which displays the settings panel for a repository view.
*/
angular.module('quay').directive('repoPanelSettings', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-view/repo-panel-settings.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, $route, ApiService, Config, Features, StateService) {
StateService.updateStateIn($scope, function(state) {
$scope.inReadOnlyMode = state.inReadOnlyMode;
});
$scope.Features = Features;
$scope.deleteDialogCounter = 0;
var getTitle = function(repo) {
return repo.kind == 'application' ? 'application' : 'image';
};
$scope.getBadgeFormat = function(format, repository) {
if (!repository) { return ''; }
var imageUrl = Config.getUrl('/repository/' + repository.namespace + '/' + repository.name + '/status');
if (!$scope.repository.is_public) {
imageUrl += '?token=' + repository.status_token;
}
var linkUrl = Config.getUrl('/repository/' + repository.namespace + '/' + repository.name);
switch (format) {
case 'svg':
return imageUrl;
case 'md':
return '[![Docker Repository on ' + Config.REGISTRY_TITLE_SHORT + '](' + imageUrl +
' "Docker Repository on ' + Config.REGISTRY_TITLE_SHORT + '")](' + linkUrl + ')';
case 'asciidoc':
return 'image:' + imageUrl + '["Docker Repository on ' + Config.REGISTRY_TITLE_SHORT + '", link="' + linkUrl + '"]';
}
return '';
};
$scope.askDelete = function() {
$scope.deleteDialogCounter++;
$scope.deleteRepoInfo = {
'counter': $scope.deleteDialogCounter
};
};
$scope.deleteRepo = function(info, callback) {
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name
};
var errorHandler = ApiService.errorDisplay(
'Could not delete ' + getTitle($scope.repository), callback);
ApiService.deleteRepository(null, params).then(function() {
callback(true);
setTimeout(function() {
document.location = '/repository/';
}, 100);
}, errorHandler);
};
$scope.askChangeAccess = function(newAccess) {
var msg = 'Are you sure you want to make this ' + getTitle($scope.repository) + ' ' +
newAccess + '?';
bootbox.confirm(msg, function(r) {
if (!r) { return; }
$scope.changeAccess(newAccess);
});
};
$scope.changeAccess = function(newAccess) {
var visibility = {
'visibility': newAccess
};
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name
};
ApiService.changeRepoVisibility(visibility, params).then(function() {
$scope.repository.is_public = newAccess == 'public';
}, ApiService.errorDisplay('Could not change visibility'));
};
// Hide State, for now, if mirroring feature-flag is not enabled.
if (Features.REPO_MIRROR) {
$scope.repoStates = ['NORMAL', 'READ_ONLY'];
}
// Only show MIRROR as an option if the feature-flag is enabled.
if (Features.REPO_MIRROR) { $scope.repoStates.push('MIRROR'); }
// Hide State, for now, if mirroring feature-flag is not enabled.
if (Features.REPO_MIRROR) {
$scope.selectedState = $scope.repository.state;
$scope.changeState = function() {
let state = {'state': $scope.selectedState};
let params = {'repository': $scope.repository.namespace + '/' + $scope.repository.name};
ApiService.changeRepoState(state, params)
.then(function() {
$scope.repository.state = $scope.selectedState;
$route.reload(); // State will eventually affect many UI elements. Reload the view.
}, ApiService.errorDisplay('Could not change state'));
}
}
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,499 @@
/**
* An element which displays the tags panel for a repository view.
*/
angular.module('quay').directive('repoPanelTags', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-view/repo-panel-tags.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'repositoryTags': '=repositoryTags',
'selectedTags': '=selectedTags',
'historyFilter': '=historyFilter',
'imagesResource': '=imagesResource',
'imageLoader': '=imageLoader',
'isEnabled': '=isEnabled',
'getImages': '&getImages'
},
controller: function($scope, $element, $filter, $location, ApiService, UIService,
VulnerabilityService, TableService, Features, StateService) {
StateService.updateStateIn($scope, function(state) {
$scope.inReadOnlyMode = state.inReadOnlyMode;
});
$scope.Features = Features;
$scope.maxTrackCount = 5;
$scope.checkedTags = UIService.createCheckStateController([], 'name');
$scope.checkedTags.setPage(0);
$scope.options = {
'predicate': 'last_modified_datetime',
'reverse': false,
'page': 0
};
$scope.iterationState = {};
$scope.tagActionHandler = null;
$scope.tagsPerPage = 25;
$scope.expandedView = false;
$scope.labelCache = {};
$scope.manifestVulnerabilities = {};
$scope.repoDelegationsInfo = null;
$scope.defcon1 = {};
$scope.hasDefcon1 = false;
var loadRepoSignatures = function() {
if (!$scope.repository || !$scope.repository.trust_enabled) {
return;
}
$scope.repoSignatureError = false;
$scope.repoDelegationsInfo = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name
};
ApiService.getRepoSignatures(null, params).then(function(resp) {
$scope.repoDelegationsInfo = resp;
}, function() {
$scope.repoDelegationsInfo = {'error': true};
});
};
var setTagState = function() {
if (!$scope.repositoryTags || !$scope.selectedTags) { return; }
// Build a list of all the tags, with extending information.
var allTags = [];
for (var tag in $scope.repositoryTags) {
if (!$scope.repositoryTags.hasOwnProperty(tag)) { continue; }
var tagData = $scope.repositoryTags[tag];
var tagInfo = $.extend(tagData, {
'name': tag,
'last_modified_datetime': TableService.getReversedTimestamp(tagData.last_modified),
'expiration_date': tagData.expiration ? TableService.getReversedTimestamp(tagData.expiration) : null,
});
allTags.push(tagInfo);
}
// Sort the tags by the predicate and the reverse, and map the information.
var imageIDs = [];
var ordered = TableService.buildOrderedItems(allTags, $scope.options,
['name'], ['last_modified_datetime', 'size']).entries;
var checked = [];
var imageMap = {};
var imageIndexMap = {};
for (var i = 0; i < ordered.length; ++i) {
var tagInfo = ordered[i];
if (!tagInfo.image_id) {
continue;
}
if (!imageMap[tagInfo.image_id]) {
imageMap[tagInfo.image_id] = [];
imageIDs.push(tagInfo.image_id)
}
imageMap[tagInfo.image_id].push(tagInfo);
if ($.inArray(tagInfo.name, $scope.selectedTags) >= 0) {
checked.push(tagInfo);
}
if (!imageIndexMap[tagInfo.image_id]) {
imageIndexMap[tagInfo.image_id] = {'start': i, 'end': i};
}
imageIndexMap[tagInfo.image_id]['end'] = i;
};
// Calculate the image tracks.
var colors = d3.scale.category10();
if (Object.keys(imageMap).length > 10) {
colors = d3.scale.category20();
}
var imageTracks = [];
var imageTrackEntries = [];
var trackEntryForImage = {};
var visibleStartIndex = ($scope.options.page * $scope.tagsPerPage);
var visibleEndIndex = (($scope.options.page + 1) * $scope.tagsPerPage);
imageIDs.sort().map(function(image_id) {
if (imageMap[image_id].length >= 2){
// Create the track entry.
var imageIndexRange = imageIndexMap[image_id];
var colorIndex = imageTrackEntries.length;
var trackEntry = {
'image_id': image_id,
'color': colors(colorIndex),
'count': imageMap[image_id].length,
'tags': imageMap[image_id],
'index_range': imageIndexRange,
'visible': visibleStartIndex <= imageIndexRange.end && imageIndexRange.start <= visibleEndIndex,
};
trackEntryForImage[image_id] = trackEntry;
imageMap[image_id]['color'] = colors(colorIndex);
// Find the track in which we can place the entry, if any.
var existingTrack = null;
for (var i = 0; i < imageTracks.length; ++i) {
// For the current track, ensure that the start and end index
// for the current entry is outside of the range of the track's
// entries. If so, then we can add the entry to the track.
var currentTrack = imageTracks[i];
var canAddToCurrentTrack = true;
for (var j = 0; j < currentTrack.entries.length; ++j) {
var currentTrackEntry = currentTrack.entries[j];
var entryInfo = imageIndexMap[currentTrackEntry.image_id];
if (Math.max(entryInfo.start, imageIndexRange.start) <= Math.min(entryInfo.end, imageIndexRange.end)) {
canAddToCurrentTrack = false;
break;
}
}
if (canAddToCurrentTrack) {
existingTrack = currentTrack;
break;
}
}
// Add the entry to the track or create a new track if necessary.
if (existingTrack) {
existingTrack.entries.push(trackEntry)
existingTrack.entryByImageId[image_id] = trackEntry;
existingTrack.endIndex = Math.max(existingTrack.endIndex, imageIndexRange.end);
for (var j = imageIndexRange.start; j <= imageIndexRange.end; j++) {
existingTrack.entryByIndex[j] = trackEntry;
}
} else {
var entryByImageId = {};
entryByImageId[image_id] = trackEntry;
var entryByIndex = {};
for (var j = imageIndexRange.start; j <= imageIndexRange.end; j++) {
entryByIndex[j] = trackEntry;
}
imageTracks.push({
'entries': [trackEntry],
'entryByImageId': entryByImageId,
'startIndex': imageIndexRange.start,
'endIndex': imageIndexRange.end,
'entryByIndex': entryByIndex,
});
}
imageTrackEntries.push(trackEntry);
}
});
$scope.imageMap = imageMap;
$scope.imageTracks = imageTracks;
$scope.imageTrackEntries = imageTrackEntries;
$scope.trackEntryForImage = trackEntryForImage;
$scope.options.page = 0;
$scope.tags = ordered;
$scope.allTags = allTags;
$scope.checkedTags = UIService.createCheckStateController(ordered, 'name');
$scope.checkedTags.setPage($scope.options.page, $scope.tagsPerPage);
$scope.checkedTags.listen(function(allChecked, pageChecked) {
$scope.selectedTags = allChecked.map(function(tag_info) {
return tag_info.name;
});
$scope.fullPageSelected = ((pageChecked.length == $scope.tagsPerPage) &&
(allChecked.length != $scope.tags.length));
$scope.allTagsSelected = ((allChecked.length > $scope.tagsPerPage) &&
(allChecked.length == $scope.tags.length));
});
$scope.checkedTags.setChecked(checked);
}
$scope.$watch('options.predicate', setTagState);
$scope.$watch('options.reverse', setTagState);
$scope.$watch('options.filter', setTagState);
$scope.$watch('options.page', function(page) {
if (page != null && $scope.checkedTags) {
$scope.checkedTags.setPage(page, $scope.tagsPerPage);
}
});
$scope.$watch('selectedTags', function(selectedTags) {
if (!selectedTags || !$scope.repository || !$scope.imageMap) { return; }
$scope.checkedTags.setChecked(selectedTags.map(function(tag) {
return $scope.repositoryTags[tag];
}));
}, true);
$scope.$watch('repository', function(updatedRepoObject, previousRepoObject) {
// Process each of the tags.
setTagState();
loadRepoSignatures();
});
$scope.$watch('repositoryTags', function(newTags, oldTags) {
if (newTags === oldTags) { return; }
// Process each of the tags.
setTagState();
loadRepoSignatures();
}, true);
$scope.clearSelectedTags = function() {
$scope.checkedTags.setChecked([]);
};
$scope.selectAllTags = function() {
$scope.checkedTags.setChecked($scope.tags);
};
$scope.constrastingColor = function(backgroundColor) {
// From: https://stackoverflow.com/questions/11068240/what-is-the-most-efficient-way-to-parse-a-css-color-in-javascript
function parseColor(input) {
m = input.match(/^#([0-9a-f]{6})$/i)[1];
return [
parseInt(m.substr(0,2),16),
parseInt(m.substr(2,2),16),
parseInt(m.substr(4,2),16)
];
}
var rgb = parseColor(backgroundColor);
// From W3C standard.
var o = Math.round(((parseInt(rgb[0]) * 299) + (parseInt(rgb[1]) * 587) + (parseInt(rgb[2]) * 114)) / 1000);
return (o > 150) ? 'black' : 'white';
};
$scope.getTrackEntryForIndex = function(it, index) {
index += $scope.options.page * $scope.tagsPerPage;
return it.entryByIndex[index];
};
$scope.trackLineExpandedClass = function(it, index, track_info) {
var entry = $scope.getTrackEntryForIndex(it, index);
if (!entry) {
return '';
}
var adjustedIndex = index + ($scope.options.page * $scope.tagsPerPage);
if (index < entry.index_range.start) {
return 'before';
}
if (index > entry.index_range.end) {
return 'after';
}
if (index >= entry.index_range.start && index < entry.index_range.end) {
return 'middle';
}
return '';
};
$scope.trackLineClass = function(it, index) {
var entry = $scope.getTrackEntryForIndex(it, index);
if (!entry) {
return '';
}
var adjustedIndex = index + ($scope.options.page * $scope.tagsPerPage);
if (index == entry.index_range.start) {
return 'start';
}
if (index == entry.index_range.end) {
return 'end';
}
if (index > entry.index_range.start && index < entry.index_range.end) {
return 'middle';
}
if (index < entry.index_range.start) {
return 'before';
}
if (index > entry.index_range.end) {
return 'after';
}
};
$scope.tablePredicateClass = function(name, predicate, reverse) {
if (name != predicate) {
return '';
}
return 'current ' + (reverse ? 'reversed' : '');
};
$scope.askDeleteTag = function(tag) {
$scope.tagActionHandler.askDeleteTag(tag);
};
$scope.askDeleteMultipleTags = function(tags) {
if (tags.length == 1) {
$scope.askDeleteTag(tags[0].name);
return;
}
$scope.tagActionHandler.askDeleteMultipleTags(tags);
};
$scope.askChangeTagsExpiration = function(tags) {
if ($scope.inReadOnlyMode) {
return;
}
$scope.tagActionHandler.askChangeTagsExpiration(tags);
};
$scope.askAddTag = function(tag) {
if ($scope.inReadOnlyMode) {
return;
}
$scope.tagActionHandler.askAddTag(tag.image_id, tag.manifest_digest);
};
$scope.showLabelEditor = function(tag) {
if ($scope.inReadOnlyMode) {
return;
}
if (!tag.manifest_digest) { return; }
$scope.tagActionHandler.showLabelEditor(tag.manifest_digest);
};
$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.commitTagFilter = function(tag) {
var r = new RegExp('^[0-9a-fA-F]{7}$');
return tag.name.match(r);
};
$scope.allTagFilter = function(tag) {
return true;
};
$scope.noTagFilter = function(tag) {
return false;
};
$scope.imageIDFilter = function(image_id, tag) {
return tag.image_id == image_id;
};
$scope.setTab = function(tab) {
$location.search('tab', tab);
};
$scope.selectTrack = function(it) {
$scope.checkedTags.checkByFilter(function(tag) {
return $scope.imageIDFilter(it.image_id, tag);
});
};
$scope.showHistory = function(checked) {
if (!checked.length) {
return;
}
$scope.historyFilter = $scope.getTagNames(checked);
$scope.setTab('history');
};
$scope.setExpanded = function(expanded) {
$scope.expandedView = expanded;
};
$scope.getTagNames = function(checked) {
var names = checked.map(function(tag) {
return tag.name;
});
return names.join(',');
};
$scope.handleLabelsChanged = function(manifest_digest) {
delete $scope.labelCache[manifest_digest];
};
$scope.loadManifestList = function(tag) {
if (tag.manifest_list_loading) {
return;
}
tag.manifest_list_loading = true;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'manifestref': tag.manifest_digest
};
ApiService.getRepoManifest(null, params).then(function(resp) {
tag.manifest_list = JSON.parse(resp['manifest_data']);
tag.manifest_list_loading = false;
}, ApiService.errorDisplay('Could not load manifest list contents'))
};
$scope.manifestsOf = function(tag) {
if (!tag.is_manifest_list) {
return [];
}
if (!tag.manifest_list) {
$scope.loadManifestList(tag);
return [];
}
if (!tag._mapped_manifests) {
// Calculate once and cache to avoid angular digest cycles.
tag._mapped_manifests = tag.manifest_list.manifests.map(function(manifest) {
return {
'raw': manifest,
'os': manifest.platform.os,
'size': manifest.size,
'digest': manifest.digest,
'description': `${manifest.platform.os} on ${manifest.platform.architecture}`,
};
});
}
return tag._mapped_manifests;
};
}
};
return directiveDefinitionObject;
});