From bb875906cb6b939d81efa05c5da42e2db47757c3 Mon Sep 17 00:00:00 2001 From: Jeff Cantrill Date: Thu, 9 Apr 2015 14:54:57 -0400 Subject: [PATCH] [DEVEXP-457] Create application from source. Includes changes from jwforres, cewong, sgoodwin --- .gitignore | 1 + assets/app/index.html | 14 +- assets/app/scripts/app.js | 13 +- .../app/scripts/controllers/catalog/images.js | 78 + .../{catalog.js => catalog/templates.js} | 2 - assets/app/scripts/controllers/create.js | 62 + .../controllers/create/createFromImage.js | 183 ++ .../scripts/controllers/newfromtemplate.js | 8 +- assets/app/scripts/directives/catalog.js | 35 + assets/app/scripts/directives/labels.js | 40 +- assets/app/scripts/directives/oscFileInput.js | 35 + .../app/scripts/directives/oscFormSection.js | 24 + .../app/scripts/directives/oscImageSummary.js | 13 + assets/app/scripts/directives/oscKeyValues.js | 162 ++ .../directives/oscResourceNameValidator.js | 35 + assets/app/scripts/directives/oscRouting.js | 35 + assets/app/scripts/filters/resources.js | 37 +- assets/app/scripts/filters/util.js | 69 + .../scripts/services/applicationGenerator.js | 269 +++ assets/app/scripts/services/data.js | 11 +- assets/app/scripts/services/nameGenerator.js | 25 + assets/app/scripts/services/navigate.js | 40 + assets/app/styles/_buttons.less | 19 + assets/app/styles/_core.less | 117 +- assets/app/styles/_tile.less | 1 + assets/app/styles/_variables.less | 2 +- assets/app/styles/main.less | 1 + assets/app/views/_labels.html | 62 - assets/app/views/_project-nav.html | 6 +- assets/app/views/_templateopt.html | 2 +- assets/app/views/catalog/_image.html | 49 + assets/app/views/catalog/images.html | 34 + .../{catalog.html => catalog/templates.html} | 9 +- assets/app/views/create.html | 45 + assets/app/views/create/fromimage.html | 222 +++ assets/app/views/directives/labels.html | 32 + .../app/views/directives/osc-file-input.html | 26 + .../views/directives/osc-form-section.html | 21 + .../views/directives/osc-image-summary.html | 6 + .../app/views/directives/osc-key-values.html | 77 + assets/app/views/directives/osc-routing.html | 83 + assets/app/views/newfromtemplate.html | 18 +- assets/test/karma.conf.js | 1 + .../controllers/create/createFromImageSpec.js | 43 + .../test/spec/directives/oscKeyValuesSpec.js | 124 ++ .../oscResourceNameValidatorSpec.js | 63 + .../test/spec/filters/defaultIfEmptySpec.js | 32 + assets/test/spec/filters/httpHttpsSpec.js | 18 + assets/test/spec/filters/valuesInSpec.js | 22 + assets/test/spec/filters/valuesNotInSpec.js | 23 + assets/test/spec/filters/yesNoSpec.js | 18 + .../spec/services/applicationGeneratorSpec.js | 353 ++++ .../test/spec/services/nameGeneratorSpec.js | 31 + assets/test/spec/spec-helper.js | 44 + examples/image-streams/image-streams.json | 41 +- .../application-template-stibuild.json | 4 +- pkg/assets/bindata.go | 1520 ++++++++++++++--- 57 files changed, 3975 insertions(+), 385 deletions(-) create mode 100644 assets/app/scripts/controllers/catalog/images.js rename assets/app/scripts/controllers/{catalog.js => catalog/templates.js} (98%) create mode 100644 assets/app/scripts/controllers/create.js create mode 100644 assets/app/scripts/controllers/create/createFromImage.js create mode 100644 assets/app/scripts/directives/oscFileInput.js create mode 100644 assets/app/scripts/directives/oscFormSection.js create mode 100644 assets/app/scripts/directives/oscImageSummary.js create mode 100644 assets/app/scripts/directives/oscKeyValues.js create mode 100644 assets/app/scripts/directives/oscResourceNameValidator.js create mode 100644 assets/app/scripts/directives/oscRouting.js create mode 100644 assets/app/scripts/services/applicationGenerator.js create mode 100644 assets/app/scripts/services/nameGenerator.js create mode 100644 assets/app/scripts/services/navigate.js create mode 100644 assets/app/styles/_buttons.less delete mode 100644 assets/app/views/_labels.html create mode 100644 assets/app/views/catalog/_image.html create mode 100644 assets/app/views/catalog/images.html rename assets/app/views/{catalog.html => catalog/templates.html} (68%) create mode 100644 assets/app/views/create.html create mode 100644 assets/app/views/create/fromimage.html create mode 100644 assets/app/views/directives/labels.html create mode 100644 assets/app/views/directives/osc-file-input.html create mode 100644 assets/app/views/directives/osc-form-section.html create mode 100644 assets/app/views/directives/osc-image-summary.html create mode 100644 assets/app/views/directives/osc-key-values.html create mode 100644 assets/app/views/directives/osc-routing.html create mode 100644 assets/test/spec/controllers/create/createFromImageSpec.js create mode 100644 assets/test/spec/directives/oscKeyValuesSpec.js create mode 100644 assets/test/spec/directives/oscResourceNameValidatorSpec.js create mode 100644 assets/test/spec/filters/defaultIfEmptySpec.js create mode 100644 assets/test/spec/filters/httpHttpsSpec.js create mode 100644 assets/test/spec/filters/valuesInSpec.js create mode 100644 assets/test/spec/filters/valuesNotInSpec.js create mode 100644 assets/test/spec/filters/yesNoSpec.js create mode 100644 assets/test/spec/services/applicationGeneratorSpec.js create mode 100644 assets/test/spec/services/nameGeneratorSpec.js create mode 100644 assets/test/spec/spec-helper.js diff --git a/.gitignore b/.gitignore index 17dbc91528c4..0ac75444665d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /openshift.local.* /.project /.vagrant +/assets/nbproject /examples/sample-app/openshift.local.* /examples/sample-app/logs/openshift.log *.swp diff --git a/assets/app/index.html b/assets/app/index.html index d43395d26ce3..b8d5ea42be6c 100644 --- a/assets/app/index.html +++ b/assets/app/index.html @@ -120,8 +120,11 @@ + + + @@ -133,14 +136,23 @@ + - + + + + + + + + + diff --git a/assets/app/scripts/app.js b/assets/app/scripts/app.js index ac1fe9397262..a5cb17a0ba87 100644 --- a/assets/app/scripts/app.js +++ b/assets/app/scripts/app.js @@ -125,12 +125,21 @@ angular .when('/project/:project/browse/services', { templateUrl: 'views/services.html' }) - .when('/project/:project/catalog', { - templateUrl: 'views/catalog.html' + .when('/project/:project/catalog/templates', { + templateUrl: 'views/catalog/templates.html' }) + .when('/project/:project/catalog/images', { + templateUrl: 'views/catalog/images.html' + }) + .when('/project/:project/create', { + templateUrl: 'views/create.html' + }) .when('/project/:project/create/fromtemplate', { templateUrl: 'views/newfromtemplate.html' }) + .when('/project/:project/create/fromimage', { + templateUrl: 'views/create/fromimage.html' + }) .when('/oauth', { templateUrl: 'views/util/oauth.html', controller: 'OAuthController' diff --git a/assets/app/scripts/controllers/catalog/images.js b/assets/app/scripts/controllers/catalog/images.js new file mode 100644 index 000000000000..8bc46fdfdd4d --- /dev/null +++ b/assets/app/scripts/controllers/catalog/images.js @@ -0,0 +1,78 @@ +'use strict'; + +/** + * @ngdoc function + * @name openshiftConsole.controller:PodsController + * @description + * # ProjectController + * Controller of the openshiftConsole + */ +angular.module('openshiftConsole') + .controller('CatalogImagesController', function ($scope, DataService, $filter, LabelFilter, imageEnvFilter, $routeParams, Logger) { + $scope.projectImageRepos = {}; + $scope.openshiftImageRepos = {}; + $scope.builders = []; + $scope.images = []; + $scope.sourceURL = $routeParams.builderfor; + + var imagesForRepos = function(imageRepos, scope) { + angular.forEach(imageRepos, function(imageRepo) { + if (imageRepo.status) { + angular.forEach(imageRepo.status.tags, function(tag) { + var imageRepoTag = tag.tag; + var image = { + imageRepo: imageRepo, + imageRepoTag: imageRepoTag, + name: imageRepo.metadata.name + ":" + imageRepoTag + }; + $scope.images.push(image); + + var categoryTags = []; + if(imageRepo.spec.tags){ + angular.forEach(imageRepo.spec.tags, function(imageTags){ + if(imageTags.annotations && imageTags.annotations.tags){ + categoryTags = imageTags.annotations.tags.split(/\s*,\s*/); + } + if (categoryTags.indexOf("builder") >= 0) { + $scope.builders.push(image); + } + }); + } + }); + Logger.info("builders", $scope.builders); + } + }); + }; + + DataService.list("imageStreams", $scope, function(imageRepos) { + $scope.projectImageRepos = imageRepos.by("metadata.name"); + imagesForRepos($scope.projectImageRepos, $scope); + + Logger.info("project image repos", $scope.projectImageRepos); + }); + + DataService.list("imageStreams", {namespace: "openshift"}, function(imageRepos) { + $scope.openshiftImageRepos = imageRepos.by("metadata.name"); + imagesForRepos($scope.openshiftImageRepos, {namespace: "openshift"}); + + Logger.info("openshift image repos", $scope.openshiftImageRepos); + }); + + + var templatesByTag = function() { + $scope.templatesByTag = {}; + angular.forEach($scope.templates, function(template) { + if (template.metadata.annotations && template.metadata.annotations.tags) { + var tags = template.metadata.annotations.tags.split(","); + angular.forEach(tags, function(tag){ + tag = $.trim(tag); + // not doing this as a map since we are dealing with things across namespaces that could have collisions on name + $scope.templatesByTag[tag] = $scope.templatesByTag[tag] || []; + $scope.templatesByTag[tag].push(template); + }); + } + }); + + Logger.info("templatesByTag", $scope.templatesByTag); + }; + }); diff --git a/assets/app/scripts/controllers/catalog.js b/assets/app/scripts/controllers/catalog/templates.js similarity index 98% rename from assets/app/scripts/controllers/catalog.js rename to assets/app/scripts/controllers/catalog/templates.js index 362ad18ae326..534bb57461f5 100644 --- a/assets/app/scripts/controllers/catalog.js +++ b/assets/app/scripts/controllers/catalog/templates.js @@ -15,8 +15,6 @@ angular.module('openshiftConsole') $scope.templatesByTag = {}; $scope.templates = []; - $scope.instantApps = []; - DataService.list("templates", $scope, function(templates) { $scope.projectTemplates = templates.by("metadata.name"); allTemplates(); diff --git a/assets/app/scripts/controllers/create.js b/assets/app/scripts/controllers/create.js new file mode 100644 index 000000000000..7efe6ad6e518 --- /dev/null +++ b/assets/app/scripts/controllers/create.js @@ -0,0 +1,62 @@ +'use strict'; + +/** + * @ngdoc function + * @name openshiftConsole.controller:PodsController + * @description + * # ProjectController + * Controller of the openshiftConsole + */ +angular.module('openshiftConsole') + .controller('CreateController', function ($scope, DataService, $filter, LabelFilter, $location, Logger) { + $scope.projectTemplates = {}; + $scope.openshiftTemplates = {}; + + $scope.templatesByTag = {}; + + $scope.sourceURLPattern = /^(ftp|http|https|git):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; + + DataService.list("templates", $scope, function(templates) { + $scope.projectTemplates = templates.by("metadata.name"); + templatesByTag(); + Logger.info("project templates", $scope.projectTemplates); + }); + + DataService.list("templates", {namespace: "openshift"}, function(templates) { + $scope.openshiftTemplates = templates.by("metadata.name"); + templatesByTag(); + Logger.info("openshift templates", $scope.openshiftTemplates); + }); + + var templatesByTag = function() { + $scope.templatesByTag = {}; + var fn = function(template) { + if (template.metadata.annotations && template.metadata.annotations.tags) { + var tags = template.metadata.annotations.tags.split(","); + angular.forEach(tags, function(tag){ + tag = $.trim(tag); + // not doing this as a map since we are dealing with things across namespaces that could have collisions on name + $scope.templatesByTag[tag] = $scope.templatesByTag[tag] || []; + $scope.templatesByTag[tag].push(template); + }); + } + }; + + angular.forEach($scope.projectTemplates, fn); + angular.forEach($scope.openshiftTemplates, fn); + + Logger.info("templatesByTag", $scope.templatesByTag); + }; + + $scope.createFromSource = function() { + if($scope.from_source_form.$valid) { + var createURI = URI.expand("/project/{project}/catalog/images{?q*}", { + project: $scope.projectName, + q: { + builderfor: $scope.from_source_url + } + }); + $location.url(createURI.toString()); + } + }; + }); diff --git a/assets/app/scripts/controllers/create/createFromImage.js b/assets/app/scripts/controllers/create/createFromImage.js new file mode 100644 index 000000000000..9e4cfb73dfaa --- /dev/null +++ b/assets/app/scripts/controllers/create/createFromImage.js @@ -0,0 +1,183 @@ +"use strict"; + +angular.module("openshiftConsole") + .controller("CreateFromImageController", function ($scope, + Logger, + $q, + $routeParams, + DataService, + Navigate, + NameGenerator, + ApplicationGenerator, + TaskList + ){ + function initAndValidate(scope){ + + if(!$routeParams.imageName){ + Navigate.toErrorPage("Cannot create from source: a base image was not specified"); + } + if(!$routeParams.imageTag){ + Navigate.toErrorPage("Cannot create from source: a base image tag was not specified"); + } + if(!$routeParams.sourceURL){ + Navigate.toErrorPage("Cannot create from source: source url was not specified"); + } + + scope.emptyMessage = "Loading..."; + scope.imageName = $routeParams.imageName; + scope.imageTag = $routeParams.imageTag; + scope.namespace = $routeParams.namespace; + scope.buildConfig = { + sourceUrl: $routeParams.sourceURL, + buildOnSourceChange: true, + buildOnImageChange: true + }; + scope.deploymentConfig = { + deployOnNewImage: true, + deployOnConfigChange: true, + envVars : { + } + }; + scope.routing = true; + scope.labels = {}; + scope.scaling = { + replicas: 1 + }; + + DataService.get("imageStreams", scope.imageName, scope, {namespace: scope.namespace}).then(function(imageRepo){ + scope.imageRepo = imageRepo; + var imageName = scope.imageTag; + DataService.get("imageStreamTags", imageRepo.metadata.name + ":" + imageName, {namespace: scope.namespace}).then(function(image){ + scope.image = image; + angular.forEach(image.dockerImageMetadata.ContainerConfig.Env, function(entry){ + var pair = entry.split("="); + scope.deploymentConfig.envVars[pair[0]] = pair[1]; + }); + }, function(){ + Navigate.toErrorPage("Cannot create from source: the specified image could not be retrieved."); + } + ); + }, + function(){ + Navigate.toErrorPage("Cannot create from source: the specified image could not be retrieved."); + }); + scope.name = NameGenerator.suggestFromSourceUrl(scope.buildConfig.sourceUrl); + } + + initAndValidate($scope); + + var ifResourcesDontExist = function(resources, namespace, scope){ + var result = $q.defer(); + var successResults = []; + var failureResults = []; + var remaining = resources.length; + + function _checkDone() { + if (remaining === 0) { + if(successResults.length > 0){ + //means some resources exist with the given nanme + result.reject(successResults); + } + else + //means no resources exist with the given nanme + result.resolve(resources); + } + } + + resources.forEach(function(resource) { + DataService.get(resource.kind, resource.metadata.name, scope, {namespace: namespace, errorNotification: false}).then( + function (data) { + successResults.push(data); + remaining--; + _checkDone(); + }, + function (data) { + failureResults.push(data); + remaining--; + _checkDone(); + } + ); + }); + return result.promise; + } + + var createResources = function(resources){ + var titles = { + started: "Creating application " + $scope.name + " in project " + $scope.projectName, + success: "Created application " + $scope.name + " in project " + $scope.projectName, + failure: "Failed to create " + $scope.name + " in project " + $scope.projectName + }; + var helpLinks = {}; + + TaskList.add(titles, helpLinks, function(){ + var d = $q.defer(); + DataService.createList(resources, $scope) + //refactor these helpers to be common for 'newfromtemplate' + .then(function(result) { + var alerts = []; + var hasErrors = false; + if (result.failure.length > 0) { + result.failure.forEach( + function(failure) { + var objectName = ""; + if (failure.data && failure.data.details) { + objectName = failure.data.details.kind + " " + failure.data.details.id; + } else { + objectName = "object"; + } + alerts.push({ + type: "error", + message: "Cannot create " + objectName + ". ", + details: failure.data.message + }); + hasErrors = true; + } + ); + } else { + alerts.push({ type: "success", message: "All resource for application " + $scope.name + + " were created successfully."}); + } + d.resolve({alerts: alerts, hasErrors: hasErrors}); + } + ); + return d.promise; + }, + function(result) { // failure + $scope.alerts = [ + { + type: "error", + message: "An error occurred creating the application.", + details: "Status: " + result.status + ". " + result.data + } + ]; + } + ); + Navigate.toProjectOverview($scope.projectName); + }; + + var elseShowWarning = function(){ + $scope.nameTaken = true; + }; + + $scope.createApp = function(){ + var resourceMap = ApplicationGenerator.generate($scope); + //init tasks + var resources = []; + angular.forEach(resourceMap, function(value, key){ + if(value !== null){ + Logger.debug("Generated resource definition:", value); + resources.push(value); + } + }); + + ifResourcesDontExist(resources, $scope.namespace, $scope).then( + createResources, + elseShowWarning + ); + + }; + + + } +); + diff --git a/assets/app/scripts/controllers/newfromtemplate.js b/assets/app/scripts/controllers/newfromtemplate.js index b4b8c018bd42..03df71225dca 100644 --- a/assets/app/scripts/controllers/newfromtemplate.js +++ b/assets/app/scripts/controllers/newfromtemplate.js @@ -65,7 +65,7 @@ angular.module('openshiftConsole') var helpLinks = {}; for (var attr in template.annotations) { var match = attr.match(helpLinkName); - var link; + var link; if (match) { link = helpLinks[match[1]] || {}; link.title = template.annotations[attr]; @@ -133,7 +133,7 @@ angular.module('openshiftConsole') ); return d.promise; }); - $location.path("project/" + $scope.projectName + "/overview"); + Navigate.toProjectOverview($scope.projectName); }, function(result) { // failure $scope.alerts = [ @@ -155,7 +155,7 @@ angular.module('openshiftConsole') var namespace = $routeParams.namespace; if (!name) { - errorPage("Cannot create from template: a template name was not specified."); + Navigate.toErrorPage("Cannot create from template: a template name was not specified."); return; } @@ -178,7 +178,7 @@ angular.module('openshiftConsole') template.labels = template.labels || {}; }, function() { - errorPage("Cannot create from template: the specified template could not be retrieved."); + Navigate.toErrorPage("Cannot create from template: the specified template could not be retrieved."); } ); }); diff --git a/assets/app/scripts/directives/catalog.js b/assets/app/scripts/directives/catalog.js index 59db22b7420e..597b91e2de11 100644 --- a/assets/app/scripts/directives/catalog.js +++ b/assets/app/scripts/directives/catalog.js @@ -26,6 +26,41 @@ angular.module('openshiftConsole') }) .modal('hide'); + }); + } + }; + }) + .directive('catalogImage', function($location, Logger) { + return { + restrict: 'E', + scope: { + image: '=', + imageRepo: '=', + imageTag: '=', + project: '=', + sourceUrl: '=' + }, + templateUrl: 'views/catalog/_image.html', + link: function(scope, elem, attrs) { + $(".select-image", elem).click(function() { + // Must trigger off of the modal's hidden event to guarantee modal has finished closing before switching screens + $(".modal", elem).on('hidden.bs.modal', function () { + scope.$apply(function() { + Logger.info(scope); + var createURI = URI.expand("/project/{project}/create/fromimage{?q*}", { + project: scope.project, + q: { + imageName: scope.imageRepo.metadata.name, + imageTag: scope.imageTag, + namespace: scope.imageRepo.metadata.namespace, + sourceURL: scope.sourceUrl + } + }); + $location.url(createURI.toString()); + }); + }) + .modal('hide'); + }); } }; diff --git a/assets/app/scripts/directives/labels.js b/assets/app/scripts/directives/labels.js index ac845271e2bd..4a741f4f2d3f 100644 --- a/assets/app/scripts/directives/labels.js +++ b/assets/app/scripts/directives/labels.js @@ -4,45 +4,9 @@ angular.module('openshiftConsole') .directive('labels', function() { return { restrict: 'E', - templateUrl: 'views/_labels.html', scope: { - labels: '=' + labels: "=" }, - }; - }) - .directive('labelValidator', function() { - return { - restrict: 'A', - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$validators.label = function(modelValue, viewValue) { - var LABEL_REGEXP = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/; - var LABEL_MAXLENGTH = 63; - var SUBDOMAIN_REGEXP = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/; - var SUBDOMAIN_MAXLENGTH = 253; - - function validateSubdomain(str) { - if (str.length > SUBDOMAIN_MAXLENGTH) { return false; } - return SUBDOMAIN_REGEXP.test(str); - } - - function validateLabel(str) { - if (str.length > LABEL_MAXLENGTH) { return false; } - return LABEL_REGEXP.test(str); - } - - if (ctrl.$isEmpty(modelValue)) { - return true; - } - var parts = viewValue.split("/"); - switch(parts.length) { - case 1: - return validateLabel(parts[0]); - case 2: - return validateSubdomain(parts[0]) && validateLabel(parts[1]); - } - return false; - }; - } + templateUrl: 'views/directives/labels.html' }; }); diff --git a/assets/app/scripts/directives/oscFileInput.js b/assets/app/scripts/directives/oscFileInput.js new file mode 100644 index 000000000000..31f9b201b5c5 --- /dev/null +++ b/assets/app/scripts/directives/oscFileInput.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('openshiftConsole') + .directive('oscFileInput', function(Logger) { + return { + restrict: 'E', + scope: { + name: "@", + model: "=", + required: "=" + }, + templateUrl: 'views/directives/osc-file-input.html', + link: function(scope, element, attrs){ + scope.supportsFileUpload = (window.File && window.FileReader && window.FileList && window.Blob); + scope.uploadError = false; + $(element).change(function(){ + var file = $('input[type=file]',this)[0].files[0]; + var reader = new FileReader(); + reader.onloadend = function(){ + scope.$apply(function(){ + scope.fileName = file.name; + scope.model = reader.result; + }); + }; + reader.onerror = function(e){ + scope.supportsFileUpload = false; + scope.uploadError = true; + Logger.error(e); + }; +// reader.readAsBinaryString(file); + reader.onerror(); + }); + } + }; + }); \ No newline at end of file diff --git a/assets/app/scripts/directives/oscFormSection.js b/assets/app/scripts/directives/oscFormSection.js new file mode 100644 index 000000000000..ca20466d1ae7 --- /dev/null +++ b/assets/app/scripts/directives/oscFormSection.js @@ -0,0 +1,24 @@ +"use strict"; + +angular.module("openshiftConsole") + .directive("oscFormSection", function () { + return { + restrict: "E", + transclude: true, + scope: { + header: "@", + about: "@", + aboutTitle: "@", + editText: "@", + expand: "@" + }, + templateUrl: "views/directives/osc-form-section.html", + link: function(scope, element, attrs){ + if(!attrs.editText) attrs.editText="Edit"; + scope.expand = attrs.expand ? true : false; + scope.toggle = function(){ + scope.expand = !scope.expand; + }; + } + }; + }); \ No newline at end of file diff --git a/assets/app/scripts/directives/oscImageSummary.js b/assets/app/scripts/directives/oscImageSummary.js new file mode 100644 index 000000000000..73f7662e12d6 --- /dev/null +++ b/assets/app/scripts/directives/oscImageSummary.js @@ -0,0 +1,13 @@ +"use strict"; + +angular.module("openshiftConsole") + .directive("oscImageSummary", function() { + return { + restrict: "E", + scope: { + resource: "=", + name: "=" + }, + templateUrl: "views/directives/osc-image-summary.html" + }; +}); \ No newline at end of file diff --git a/assets/app/scripts/directives/oscKeyValues.js b/assets/app/scripts/directives/oscKeyValues.js new file mode 100644 index 000000000000..7ba2f396af64 --- /dev/null +++ b/assets/app/scripts/directives/oscKeyValues.js @@ -0,0 +1,162 @@ +"use strict"; + +angular.module("openshiftConsole") + .controller("KeyValuesEntryController", function($scope){ + $scope.editing = false; + $scope.edit = function(){ + $scope.originalValue = $scope.value; + $scope.editing = true; + }; + $scope.cancel= function(){ + $scope.value = $scope.originalValue; + $scope.editing = false; + }; + $scope.update = function(key, value, entries){ + if(value){ + entries[key] = value; + $scope.editing = false; + } + }; + }) + .controller("KeyValuesController", function($scope){ + var added = {}; + $scope.allowDelete = function(value){ + if($scope.deletePolicy === "never") return false; + if($scope.deletePolicy === "added"){ + return added[value] !== undefined; + } + return true; + }; + $scope.addEntry = function() { + if($scope.key && $scope.value){ + var readonly = $scope.readonlyKeys.split(","); + if(readonly.indexOf($scope.key) !== -1){ + return; + } + added[$scope.key] = ""; + $scope.entries[$scope.key] = $scope.value; + $scope.key = null; + $scope.value = null; + $scope.form.$setPristine(); + $scope.form.$setUntouched(); + $scope.form.$setValidity(); + } + }; + $scope.deleteEntry = function(key) { + if ($scope.entries[key]) { + delete $scope.entries[key]; + delete added[key]; + } + }; + }) + .directive("oscInputValidator", function(){ + + var validators = { + always: function(modelValue, viewValue){ + return true; + }, + env: function(modelValue, viewValue){ + var C_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/i; + if(modelValue === undefined || modelValue === null || modelValue.trim().length === 0) + return true; + return C_IDENTIFIER_RE.test(viewValue); + }, + label: function(modelValue, viewValue) { + var LABEL_REGEXP = /^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$/; + var LABEL_MAXLENGTH = 63; + var SUBDOMAIN_REGEXP = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/; + var SUBDOMAIN_MAXLENGTH = 253; + + function validateSubdomain(str) { + if (str.length > SUBDOMAIN_MAXLENGTH) { return false; } + return SUBDOMAIN_REGEXP.test(str); + } + + function validateLabel(str) { + if (str.length > LABEL_MAXLENGTH) { return false; } + return LABEL_REGEXP.test(str); + } + + if (modelValue === undefined || modelValue === null || modelValue.trim().length === 0) { + return true; + } + var parts = viewValue.split("/"); + switch(parts.length) { + case 1: + return validateLabel(parts[0]); + case 2: + return validateSubdomain(parts[0]) && validateLabel(parts[1]); + } + return false; + } + }; + return { + require: ["ngModel", "^oscKeyValues"], + restrict: "A", + link: function(scope, elm, attrs, controllers) { + var ctrl = controllers[0]; + var oscKeyValues = controllers[1]; + if(attrs.oscInputValidator === 'key'){ + ctrl.$validators.oscKeyValid = validators[oscKeyValues.scope.keyValidator]; + }else if(attrs.oscInputValidator === 'value'){ + ctrl.$validators.oscValueValid = validators[oscKeyValues.scope.valueValidator]; + } + } + }; + }) + /** + * A Directive for displaying key/value pairs. Configuration options + * via attributes: + * delimiter: the value to use to separate key/value pairs when displaying + * (e.g. foo:bar). Default: ":" + * keyTitle: The value to use as the key input's placeholder. Default: Name + * editable: true if the intention is to display values only otherwise false (default) + * keyValidaor: The validator to use for validating keys + * - always: Any value is allowed (Default). + * - env: Validate as an ENV var /^[A-Za-z_][A-Za-z0-9_]*$/i + * - label: Validate as a label + * deletePolicy: + * - always: allow any key/value pair (Default) + * - added: allow any added not originally in entries + * - never: disallow any entries being deleted + * readonlyKeys: A comma delimted list of keys that are readonly + * keyValidationTooltip: The tool tip to display when the key validation message is visible + */ + .directive("oscKeyValues", function() { + return { + restrict: "E", + scope: { + keyTitle: "@", + entries: "=", + delimiter: "@", + editable: "@", + keyValidator: "@", + valueValidator: "@", + deletePolicy: "@", + readonlyKeys: "@", + keyValidationTooltip: "@", + valueValidationTooltip: "@" + }, + controller: function($scope){ + this.scope = $scope; + }, + templateUrl: "views/directives/osc-key-values.html", + compile: function(element, attrs){ + if(!attrs.delimiter){attrs.delimiter = ":";} + if(!attrs.keyTitle){attrs.keyTitle = "Name";} + if(!attrs.editable || attrs.editable === "true"){ + attrs.editable = true; + }else{ + attrs.editable = false; + } + if(!attrs.keyValidator){attrs.keyValidator = "always";} + if(!attrs.valueValidator){attrs.valueValidator = "always";} + if(["always", "added", "none"].indexOf(attrs.deletePolicy) === -1){ + attrs.deletePolicy = "always"; + } + if(!attrs.readonlyKeys){ + attrs.readonlyKeys = ""; + } + } + }; + }); \ No newline at end of file diff --git a/assets/app/scripts/directives/oscResourceNameValidator.js b/assets/app/scripts/directives/oscResourceNameValidator.js new file mode 100644 index 000000000000..ab738ec5fbac --- /dev/null +++ b/assets/app/scripts/directives/oscResourceNameValidator.js @@ -0,0 +1,35 @@ +"use strict"; + +angular.module("openshiftConsole") + .directive("oscResourceNameValidator", function(){ + + //github.com/GoogleCloudPlatform/kubernetes/pkg/util/validation.go + //limiting to valid service names as LCD + var maxNameLength = 24; + var VALID_NAME_RE = /^[a-z]([-a-z0-9]*[a-z0-9])?/i; + return { + + require: "ngModel", + link: function(scope, elm, attrs, ctrl) { + ctrl.$validators.oscResourceNameValidator = function(modelValue, viewValue){ + if(ctrl.$isEmpty(modelValue)){ + return false; + } + if(viewValue === null){ + return false; + } + if(ctrl.$isEmpty(viewValue.trim())){ + return false; + } + if(modelValue.length <= maxNameLength){ + + if(VALID_NAME_RE.test(viewValue) && viewValue.indexOf(" ") === -1){ + return true; + } + + } + return false; + }; + } + }; + }); \ No newline at end of file diff --git a/assets/app/scripts/directives/oscRouting.js b/assets/app/scripts/directives/oscRouting.js new file mode 100644 index 000000000000..85f78d404df1 --- /dev/null +++ b/assets/app/scripts/directives/oscRouting.js @@ -0,0 +1,35 @@ +"use strict"; + +angular.module("openshiftConsole") + /** + * Provides a wigit for entering route information + * + * model: The model for the input. The model will either use or add the + * following keys: + * { + * uri: "", + * customCerts: false, //true|false + * certificate: "", + * key: "", + * caCertificate: "" + * } + * uri-disabled: An expression that will make the URI text input + * disabled. Enabled by default + * security-model: A model of the custom certificate and private key in + * the form of: + */ + .directive("oscRouting", function(){ + return { + require: '^form', + restrict: 'E', + scope: { + route: "=model", + uriDisabled: "=", + uriRequired: "=" + }, + templateUrl: 'views/directives/osc-routing.html', + link: function(scope, element, attrs, formCtl){ + scope.form = formCtl; + } + }; + }); \ No newline at end of file diff --git a/assets/app/scripts/filters/resources.js b/assets/app/scripts/filters/resources.js index 8e0c5fc78cbd..c524061fdb05 100644 --- a/assets/app/scripts/filters/resources.js +++ b/assets/app/scripts/filters/resources.js @@ -3,6 +3,18 @@ angular.module('openshiftConsole') .filter('annotation', function() { return function(resource, key) { + if (resource && resource.spec && resource.spec.tags && key.indexOf(".") !== -1){ + var tagAndKey = key.split("."); + var tags = resource.spec.tags; + for(var i=0; i < tags.length; ++i){ + var tag = tags[i]; + var tagName = tagAndKey[0]; + var tagKey = tagAndKey[1]; + if(tagName === tag.name && tag.annotations){ + return tag.annotations[tagKey]; + } + } + } if (resource && resource.metadata && resource.metadata.annotations) { return resource.metadata.annotations[key]; } @@ -15,8 +27,9 @@ angular.module('openshiftConsole') }; }) .filter('tags', function(annotationFilter) { - return function(resource) { - var tags = annotationFilter(resource, "tags"); + return function(resource, annotationKey) { + annotationKey = annotationKey || "tags"; + var tags = annotationFilter(resource, annotationKey); if (!tags) { return []; } @@ -43,12 +56,16 @@ angular.module('openshiftConsole') }; }) .filter('iconClass', function(annotationFilter) { - return function(resource, kind) { - var icon = annotationFilter(resource, "iconClass"); + return function(resource, kind, annotationKey) { + annotationKey = annotationKey || "iconClass"; + var icon = annotationFilter(resource, annotationKey); if (!icon) { if (kind === "template") { return "fa fa-bolt"; } + if (kind === "image") { + return "fa fa-cube"; + } else { return ""; } @@ -81,6 +98,18 @@ angular.module('openshiftConsole') } }; }) + .filter('imageEnv', function() { + return function(image, envKey) { + var envVars = image.dockerImageMetadata.Config.Env; + for (var i = 0; i < envVars.length; i++) { + var keyValue = envVars[i].split("="); + if (keyValue[0] === envKey) { + return keyValue[1]; + } + } + return null; + }; + }) .filter('buildForImage', function() { return function(image, builds) { // TODO concerned that this gets called anytime any data is changed on the scope, whether its relevant changes or not diff --git a/assets/app/scripts/filters/util.js b/assets/app/scripts/filters/util.js index d0d2d1fe6a54..79f2c12113c9 100644 --- a/assets/app/scripts/filters/util.js +++ b/assets/app/scripts/filters/util.js @@ -1,6 +1,27 @@ 'use strict'; angular.module('openshiftConsole') + /** + * Replace special chars with underscore (e.g. '.') + * @returns {Function} + */ + .filter("underscore", function(){ + return function(value){ + return value.replace(/\./g, '_'); + }; + }) + .filter("defaultIfBlank", function(){ + return function(input, defaultValue){ + if(input === null) return defaultValue; + if(typeof input !== "string"){ + input = String(input); + } + if(input.trim().length === 0){ + return defaultValue; + } + return input; + }; + }) .filter('hashSize', function() { return function(hash) { if(!hash) { return 0; } @@ -122,4 +143,52 @@ angular.module('openshiftConsole') } } }; + }) + .filter('httpHttps', function() { + return function(isSecure) { + return isSecure ? 'https://' : 'http://'; + }; + }) + .filter('yesNo', function() { + return function(isTrue) { + return isTrue ? 'Yes' : 'No'; + }; + }) + /** + * Filter a hash of values + * + * @param {Hash} entries A Hash to filter + * @param {String} keys A comma delimited string of keys to evaluate against + * @returns {Hash} A filtered set where the keys of those in keys + */ + .filter("valuesIn", function(){ + return function(entries, keys){ + var readonly = keys.split(","); + var result = {}; + angular.forEach(entries, function(value, key){ + if( readonly.indexOf(key) !== -1){ + result[key] = value; + } + }); + return result; + }; + }) + /** + * Filter a hash of values + * + * @param {Hash} entries A Hash to filter + * @param {String} keys A comma delimited string of keys to evaluate against + * @returns {Hash} A filtered set where the keys of those not in keys + */ + .filter("valuesNotIn", function(){ + return function(entries, keys){ + var readonly = keys.split(","); + var result = {}; + angular.forEach(entries, function(value, key){ + if( readonly.indexOf(key) === -1){ + result[key] = value; + } + }); + return result; + }; }); diff --git a/assets/app/scripts/services/applicationGenerator.js b/assets/app/scripts/services/applicationGenerator.js new file mode 100644 index 000000000000..ee0763143798 --- /dev/null +++ b/assets/app/scripts/services/applicationGenerator.js @@ -0,0 +1,269 @@ +"use strict"; + +angular.module("openshiftConsole") + + .service("ApplicationGenerator", function(DataService){ + var osApiVersion = DataService.osApiVersion; + var k8sApiVersion = DataService.k8sApiVersion; + + var scope = {}; + + scope._generateSecret = function(){ + //http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4()+s4()+s4()+s4(); + }; + + /** + * Find the 'first' port of exposed ports. + * @param ports list of ports (e.g {containerPort: 80, protocol: "tcp"}) + * @return {integer} The port/protocol pair of the lowest conainer port + */ + scope._getFirstPort = function(ports){ + var first = "None"; + ports.forEach(function(port){ + if(first === "None"){ + first = port; + }else{ + if(port.containerPort < first.containerPort){ + first = port; + } + } + } + ); + return first; + }; + + /** + * Generate resource definitions to support the given input + * @param {type} input + * @returns Hash of resource definitions + */ + scope.generate = function(input){ + //map ports to k8s structure + var ports = []; + angular.forEach(input.image.dockerImageMetadata.ContainerConfig.ExposedPorts, function(value, key){ + var parts = key.split("/"); + if(parts.length === 1){ + parts.push("tcp"); + } + ports.push( + { + containerPort: parseInt(parts[0]), + name: input.name + "-" + parts[1] + "-" + parts[0], + protocol: parts[1] + }); + }); + + //augment labels + input.labels.name = input.name; + input.labels.generatedby = "OpenShiftWebConsole"; + + var imageSpec; + if(input.buildConfig.sourceUrl !== null){ + imageSpec = { + name: input.name, + tag: "latest", + toString: function(){ + return this.name + ":" + this.tag; + } + }; + } + + var resources = { + imageRepo: scope._generateImageRepo(input), + buildConfig: scope._generateBuildConfig(input, imageSpec, input.labels), + deploymentConfig: scope._generateDeploymentConfig(input, imageSpec, ports, input.labels), + service: scope._generateService(input, input.name, scope._getFirstPort(ports)) + }; + resources.route = scope._generateRoute(input, input.name, resources.service.metadata.name); + return resources; + }; + + scope._generateRoute = function(input, name, serviceName){ + if(!input.routing) return null; + return { + kind: "Route", + apiVersion: osApiVersion, + metadata: { + name: name, + labels: input.labels + }, + serviceName: serviceName, + tls: { + termination: "unsecure" + } + }; + }; + + scope._generateDeploymentConfig = function(input, imageSpec, ports, labels){ + var env = []; + angular.forEach(input.deploymentConfig.envVars, function(value, key){ + env.push({name: key, value: value}); + }); + labels = angular.copy(labels); + labels.deploymentconfig = input.name; + + var deploymentConfig = { + apiVersion: osApiVersion, + kind: "DeploymentConfig", + metadata: { + name: input.name, + labels: labels + }, + template: { + controllerTemplate: { + podTemplate: { + desiredState: { + manifest: { + containers: [ + { + image: imageSpec.toString(), + name: input.name, + ports: ports, + env: env + } + ], + version: k8sApiVersion + } + }, + labels: labels + }, + replicaSelector: { + deploymentconfig: input.name + }, + replicas: input.scaling.replicas + }, + strategy: { + type: "Recreate" + } + }, + triggers: [] + }; + if(input.deploymentConfig.deployOnNewImage){ + deploymentConfig.triggers.push( + { + type: "ImageChange", + imageChangeParams: { + automatic: true, + containerNames: [ + input.name + ], + from: { + name: imageSpec.name + }, + tag: imageSpec.tag + } + } + ); + } + if(input.deploymentConfig.deployOnConfigChange){ + deploymentConfig.triggers.push({type: "ConfigChange"}); + } + return deploymentConfig; + }; + + scope._generateBuildConfig = function(input, imageSpec, labels){ + var dockerSpec = input.imageRepo.status.dockerImageRepository + ":" + input.imageTag; + var triggers = [ + { + generic: { + secret: scope._generateSecret() + }, + type: "generic" + } + ]; + if(input.buildConfig.buildOnSourceChange){ + triggers.push({ + github: { + secret: scope._generateSecret() + }, + type: "github" + } + ); + } + if(input.buildConfig.buildOnImageChange){ + triggers.push({ + imageChange: { + image: dockerSpec, + from:{ + name: input.imageName + }, + tag: input.imageTag + }, + type: "imageChange" + }); + } + return { + apiVersion: osApiVersion, + kind: "BuildConfig", + metadata: { + name: input.name, + labels: labels + }, + parameters: { + output: { + to: { + name: imageSpec.name + } + }, + source: { + git: { + ref: "master", + uri: input.buildConfig.sourceUrl + }, + type: "Git" + }, + strategy: { + type: "STI", + stiStrategy: { + image: dockerSpec + } + } + }, + triggers: triggers + }; + }; + + scope._generateImageRepo = function(input){ + return { + apiVersion: osApiVersion, + kind: "ImageRepository", + metadata: { + name: input.name, + labels: input.labels + } + }; + }; + + scope._generateService = function(input, serviceName, port){ + var service = { + kind: "Service", + apiVersion: k8sApiVersion, + metadata: { + name: serviceName, + labels: input.labels + }, + spec: { + selector: { + deploymentconfig: input.name + } + } + }; + if(port === 'None'){ + service.spec.portalIP = 'None'; + }else{ + service.spec.port = port.containerPort; + service.spec.containerPort = port.containerPort; + service.spec.protocol = port.protocol; + } + return service; + }; + + return scope; + } +); \ No newline at end of file diff --git a/assets/app/scripts/services/data.js b/assets/app/scripts/services/data.js index 64168a79fc31..7e29f5cf1442 100644 --- a/assets/app/scripts/services/data.js +++ b/assets/app/scripts/services/data.js @@ -2,6 +2,7 @@ angular.module('openshiftConsole') .factory('DataService', function($http, $ws, $rootScope, $q, API_CFG, Notification, Logger) { + function Data(array) { this._data = {}; this._objectsByAttribute(array, "metadata.name", this._data); @@ -89,7 +90,11 @@ angular.module('openshiftConsole') var self = this; $rootScope.$on( "$routeChangeStart", function(event, next, current) { self._watchWebsocketRetriesMap = {}; - }); + }); + + this.osApiVersion = "v1beta1"; + this.k8sApiVersion = "v1beta3"; + } // type: API type (e.g. "pods") @@ -221,6 +226,9 @@ angular.module('openshiftConsole') // http - options to pass to the inner $http call // errorNotification - will popup an error notification if the API request fails (default true) DataService.prototype.get = function(type, name, context, opts) { + if(this._objectType(type) !== undefined){ + type = this._objectType(type); + } opts = opts || {}; var force = !!opts.force; @@ -669,6 +677,7 @@ angular.module('openshiftConsole') deploymentConfigs: API_CFG.openshift, images: API_CFG.openshift, imageRepositories: API_CFG.openshift, // DEPRECATED, leave here until removed from API + imageRepositoryTags: API_CFG.openshift, imageStreams: API_CFG.openshift, imageStreamImages: API_CFG.openshift, imageStreamTags: API_CFG.openshift, diff --git a/assets/app/scripts/services/nameGenerator.js b/assets/app/scripts/services/nameGenerator.js new file mode 100644 index 000000000000..5ef08dcf59ad --- /dev/null +++ b/assets/app/scripts/services/nameGenerator.js @@ -0,0 +1,25 @@ +"use strict"; + +angular.module("openshiftConsole") + .service("NameGenerator", function(){ + return { + + /** + * Get a name suggestion for resources based on the the source URL + * + * @param {String} sourceUrl the sourceURL + * @param {Array} kinds the kinds of resources to check + * @param {String} the namespace to use when quering for existence of a resource + * @returns {String} a suggested name + */ + suggestFromSourceUrl: function(sourceUrl){ + var projectName = sourceUrl.substr(sourceUrl.lastIndexOf("/")+1, sourceUrl.length); + var index = projectName.lastIndexOf("."); + if(index !== -1){ + projectName = projectName.substr(0,index); + } + return projectName; + } + }; + }); + diff --git a/assets/app/scripts/services/navigate.js b/assets/app/scripts/services/navigate.js new file mode 100644 index 000000000000..7bdd1afe02da --- /dev/null +++ b/assets/app/scripts/services/navigate.js @@ -0,0 +1,40 @@ +"use strict"; + +angular.module("openshiftConsole") + .service("Navigate", function($location){ + return { + /** + * Navigate and display the error page. + * + * @param {type} message The message to display to the user + * @param {type} errorCode An optional error code to display + * @returns {undefined} + */ + toErrorPage: function(message, errorCode) { + var redirect = URI('error').query({ + error_description: message, + error: errorCode + }).toString(); + $location.url(redirect); + }, + /** + * Navigate and display the project overview page. + * + * @param {type} projectName the projedt name + * @returns {undefined} + */ + toProjectOverview: function(projectName){ + $location.path(this.projectOverviewURL(projectName)); + }, + + /** + * Return the URL for the project overview + * + * @param {type} projectName + * @returns {String} a URL string for the project overview + */ + projectOverviewURL: function(projectName){ + return "project/" + encodeURIComponent(projectName) + "/overview"; + } + }; + }); \ No newline at end of file diff --git a/assets/app/styles/_buttons.less b/assets/app/styles/_buttons.less new file mode 100644 index 000000000000..ada99e49bf91 --- /dev/null +++ b/assets/app/styles/_buttons.less @@ -0,0 +1,19 @@ +// Misc +// -------------------------------------------------- +.btn-file { + position: relative; + overflow: hidden; + input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + cursor: inherit; + display: block; + } + } diff --git a/assets/app/styles/_core.less b/assets/app/styles/_core.less index fab9d16fe419..497243dc4f1b 100644 --- a/assets/app/styles/_core.less +++ b/assets/app/styles/_core.less @@ -16,10 +16,12 @@ html, body { } .container-main { background-color: @body-bg; + padding-bottom: @grid-gutter-width * 2; .flex(@columns: 1); } #content-wrap > .container { margin-top: 35px; + margin-bottom: @grid-gutter-width * 2; h1 { margin-top: 10px; } @@ -143,9 +145,10 @@ html, body { // Create App -// -------------- +// -------------------------------------------------- -.create-from-template { +.create-from-template, +.create-from-image { .template-name { text-align: right; span.fa { @@ -160,37 +163,42 @@ html, body { span.fa.visible-xs-inline { margin-right: 10px; } -} - -.flow { - display: table; - width: 100%; - > .flow-block { - display: inline-block; - &.right { - font-size: @font-size-small; - font-weight: normal; - } - .action { + .flow { + border-top: 1px solid rgba(0, 0, 0, 0.15); + display: table; + margin-top: @grid-gutter-width * 1.5; + width: 100%; + > .flow-block { + display: inline-block; + &.right { font-size: @font-size-small; font-weight: normal; - // &:extend(.action-inline); } - > ul.list-inline { - margin-bottom: 0; - > li { - font-size: @font-size-small; - text-align: left; + .action { + font-size: @font-size-small; + font-weight: normal; + // &:extend(.action-inline); + } + > ul.list-inline { + margin-bottom: 0; + > li { + font-size: @font-size-small; + text-align: left; + } } } } } + @media (min-width: 767px) { - .flow > .flow-block { - display: table-cell; - &.right { - // float: right; - text-align: right; + .create-from-template, + .create-from-image { + .flow > .flow-block { + display: table-cell; + &.right { + // float: right; + text-align: right; + } } } } @@ -215,7 +223,8 @@ html, body { } } -// Modals // +// Models +// -------------------------------------------------- .modal { &.modal-create { .modal-content { @@ -224,15 +233,15 @@ html, body { background-color: transparent; } .modal-body { - .template-icon { + .template-icon, .image-icon { text-align: center; } - .template-icon { + .template-icon, .image-icon { font-size: 80px; line-height: 80px; } @media (min-width:@screen-sm-min) { - .template-icon { + .template-icon, .image-icon { font-size: 130px; line-height: 130px; } @@ -255,10 +264,10 @@ html, body { } // Misc -// ------------ - +// -------------------------------------------------- .action-inline { margin-left: @action-link-inline-margin; + font-size: @font-size-small; i.pficon, i.fa { color: @btn-default-color; margin-right: 5px; @@ -274,13 +283,22 @@ code { white-space: normal; } .gutter-top-bottom { - padding: 15px 0; + padding: @grid-gutter-width / 2 0; + &.gutter-top-bottom-2x { + padding: @grid-gutter-width 0; + } } .gutter-top { - padding-top: 15px; + padding-top: @grid-gutter-width / 2; + &.gutter-top-2x { + padding-top: @grid-gutter-width; + } } .gutter-bottom { - padding-bottom: 15px; + padding-bottom: @grid-gutter-width / 2; + &.gutter-bottom-2x { + padding-bottom: @grid-gutter-width; + } } select:invalid { box-shadow: none; @@ -290,7 +308,6 @@ select:invalid { text-overflow: ellipsis; white-space: nowrap; } - .well > { h1, h2, h3, h4, h5 { &:first-child { @@ -299,7 +316,6 @@ select:invalid { } } - .attention-message { background-color: lighten(@brand-primary, 20%); border: 1px solid darken(@brand-primary, 10%); @@ -323,4 +339,31 @@ select:invalid { .short-id { background-color: #f1f1f1; color: #666; -} \ No newline at end of file +} +.input-number { + width: 60px; +} + + +// +// Component animations +// -------------------------------------------------- +.fade { + opacity: 0; + .transition(opacity 0.2s ease 0s); + &.in { + opacity: 1; + } +} +.collapse { + display: none; + &.in { + display: block; + } +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + .transition(height .10s ease); +} diff --git a/assets/app/styles/_tile.less b/assets/app/styles/_tile.less index ec8babf625ad..a7bd1a09da20 100644 --- a/assets/app/styles/_tile.less +++ b/assets/app/styles/_tile.less @@ -58,6 +58,7 @@ .tile.tile-status { background-color: @status-bg-color; border-top: 5px solid @status-border-color; + margin-top: @grid-gutter-width; } .tile-click { diff --git a/assets/app/styles/_variables.less b/assets/app/styles/_variables.less index 16b6edb02297..f0263321a80a 100644 --- a/assets/app/styles/_variables.less +++ b/assets/app/styles/_variables.less @@ -7,7 +7,7 @@ // OpenShift Console specific -@action-link-inline-margin: 15px; +@action-link-inline-margin: 5px; @console-panel-color: #fff; @console-bright-blue: #00a8e1; @console-dark-blue: #006e9c; diff --git a/assets/app/styles/main.less b/assets/app/styles/main.less index a316f0799dd5..f027bcbbf211 100644 --- a/assets/app/styles/main.less +++ b/assets/app/styles/main.less @@ -60,6 +60,7 @@ @import "_openshift-icon.less"; @import "_tile.less"; @import "_messages.less"; +@import "_buttons.less"; @import "_pods.less"; @import "_responsive-utilities.less"; @import "_sidebar.less"; diff --git a/assets/app/views/_labels.html b/assets/app/views/_labels.html deleted file mode 100644 index d73aa497bcdd..000000000000 --- a/assets/app/views/_labels.html +++ /dev/null @@ -1,62 +0,0 @@ -
-
-
-
-

Labels

-
- -
-
-
-
- - -
- : -
- - -
- -
- Please enter a valid object label - - - - - - -
-
- -
    -
  • - template - {{ labels.template }} -
  • -
  • - {{ key }} - {{ value }} - -
  • -
-
-
    -
  • - template - {{ labels.template }} -
  • -
  • - {{ key }} - {{ value }} -
  • -
-
-
diff --git a/assets/app/views/_project-nav.html b/assets/app/views/_project-nav.html index 3c1c8d814791..01cecb2b8a09 100644 --- a/assets/app/views/_project-nav.html +++ b/assets/app/views/_project-nav.html @@ -20,13 +20,13 @@
- + Create - + Create @@ -39,4 +39,4 @@
- \ No newline at end of file + diff --git a/assets/app/views/_templateopt.html b/assets/app/views/_templateopt.html index 4d369db1e17e..e49d5bdda2b3 100644 --- a/assets/app/views/_templateopt.html +++ b/assets/app/views/_templateopt.html @@ -5,7 +5,7 @@

Parameters

diff --git a/assets/app/views/catalog/_image.html b/assets/app/views/catalog/_image.html new file mode 100644 index 000000000000..368e450b86cc --- /dev/null +++ b/assets/app/views/catalog/_image.html @@ -0,0 +1,49 @@ +
+
+
+
+ +
+
+

+ {{imageRepo.metadata.name}}:{{imageTag}} +

+

{{imageRepo | annotation : (imageTag + '.description')}}

+
+
+
+ +
\ No newline at end of file diff --git a/assets/app/views/catalog/images.html b/assets/app/views/catalog/images.html new file mode 100644 index 000000000000..f451e0401f77 --- /dev/null +++ b/assets/app/views/catalog/images.html @@ -0,0 +1,34 @@ +
+
+
+
+
+ +

Select a builder image

+
+
+
There are no builder images to select from. To add a builder to your project run osc create -f <image_repository_file> -n {{projectName}}
+
+
+ +
+
+
+

+ All images + + + + +

+
+ +
+
+
+
+
diff --git a/assets/app/views/catalog.html b/assets/app/views/catalog/templates.html similarity index 68% rename from assets/app/views/catalog.html rename to assets/app/views/catalog/templates.html index f4eb87139701..2d14dafeb65c 100644 --- a/assets/app/views/catalog.html +++ b/assets/app/views/catalog/templates.html @@ -1,5 +1,5 @@
-
+
@@ -12,14 +12,7 @@

Select a template

There are no templates to select from. To add a template to your project run osc create -f <template_file> -n {{projectName}}
-
-

Instant apps

-
- -
-
-

All templates

diff --git a/assets/app/views/create.html b/assets/app/views/create.html new file mode 100644 index 000000000000..bdd0db12bce2 --- /dev/null +++ b/assets/app/views/create.html @@ -0,0 +1,45 @@ +
+
+
+
+
+

Create from ...

+ + +
+

Source repository

+
+
+
+ + + + + + +
+ Please enter a valid URL +
+
+
+ +
+
+

Instant apps

+
+ +
+
+ There are no instant apps available to create. +
+ Browse all templates... +
+
+
+
+
+
+
+
+
diff --git a/assets/app/views/create/fromimage.html b/assets/app/views/create/fromimage.html new file mode 100644 index 000000000000..b3811179925f --- /dev/null +++ b/assets/app/views/create/fromimage.html @@ -0,0 +1,222 @@ +
+
+
+
+
+
+ +
+
+ +
+
+

+ Name +

+
+ +
+

Used to uniquely identify within this project all the resources created to support the application.

+
+ Please enter a valid name. +

A valid name is applied to all generated resources and is an alphanumeric (a-z, and 0-9) string, with a maximum length of 24 characters, with the '-' character allowed anywhere except the first or last character.

+
+
+
+ This name is already in use within the project. Please choose a different name. +
+
+ + +
+
+ + {{routing | yesNo}} +
+
+
+
+ +
+
+
+ + + + +
+

Autodeploy when

+
+ + {{deploymentConfig.deployOnNewImage | yesNo}} +
+
+ + {{deploymentConfig.deployOnConfigChange | yesNo}} +
+

Environment Variables + + + +

+ + +
+
+

Autodeploy when

+
+ +
+
+ +
+
+

Environment Variables + + + +

+ +
+
+
+ + + + +
+
+ + {{buildConfig.sourceUrl | defaultIfBlank: "Not Specified"}} +
+
+ + {{buildConfig.buildOnSourceChange | yesNo}} +
+
+ + {{buildConfig.buildOnImageChange | yesNo}} +
+
+
+
+ + {{buildConfig.sourceUrl}} +
+
+ +
+
+ +
+
+
+ + + + +
+
+ + {{scaling.replicas}} +
+
+
+ +
+ Replicas must be an integer value greater than or equal to 0 +
+
+
+ + + + +
+
+ + After creation, these settings can only be modified through the osc command. +
+ + + Cancel +
+
+
+ {{ emptyMessage }} +
+
+
+
+
+
+
diff --git a/assets/app/views/directives/labels.html b/assets/app/views/directives/labels.html new file mode 100644 index 000000000000..a5c3973ace3c --- /dev/null +++ b/assets/app/views/directives/labels.html @@ -0,0 +1,32 @@ + +
+ + + + +
+
+
+ None +
+ + +
+
diff --git a/assets/app/views/directives/osc-file-input.html b/assets/app/views/directives/osc-file-input.html new file mode 100644 index 000000000000..881f88db58e2 --- /dev/null +++ b/assets/app/views/directives/osc-file-input.html @@ -0,0 +1,26 @@ +
+
+ + + Browse… + + + +
+ +
+ There was an error reading the file. Please copy the file content into the text area. +
+
diff --git a/assets/app/views/directives/osc-form-section.html b/assets/app/views/directives/osc-form-section.html new file mode 100644 index 000000000000..27f2c2ff7e39 --- /dev/null +++ b/assets/app/views/directives/osc-form-section.html @@ -0,0 +1,21 @@ +
+
+

{{header}}

+
+ +
+ +
\ No newline at end of file diff --git a/assets/app/views/directives/osc-image-summary.html b/assets/app/views/directives/osc-image-summary.html new file mode 100644 index 000000000000..94555a65718d --- /dev/null +++ b/assets/app/views/directives/osc-image-summary.html @@ -0,0 +1,6 @@ +

{{ name || resource.metadata.name }}

+ +
{{ resource | description }}
\ No newline at end of file diff --git a/assets/app/views/directives/osc-key-values.html b/assets/app/views/directives/osc-key-values.html new file mode 100644 index 000000000000..b26d781caf54 --- /dev/null +++ b/assets/app/views/directives/osc-key-values.html @@ -0,0 +1,77 @@ +
+
+
+
+ +
+ {{delimiter}} +
+ +
+ +
+ Please enter a valid key + + + + + + +
+
+ Please enter a valid value + + + + + + +
+
+ +
+
    +
  • + {{key}} + {{ value }} +
  • +
+
\ No newline at end of file diff --git a/assets/app/views/directives/osc-routing.html b/assets/app/views/directives/osc-routing.html new file mode 100644 index 000000000000..78e6488d5529 --- /dev/null +++ b/assets/app/views/directives/osc-routing.html @@ -0,0 +1,83 @@ + +
+
+ +
+ + + +
+ +
+ Please enter a valid uri for this route. +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+ A certificate, private key and CA certificate are required when providing your own certs. +
+
+
+
+
diff --git a/assets/app/views/newfromtemplate.html b/assets/app/views/newfromtemplate.html index c4529ab722db..8b8da801f15c 100644 --- a/assets/app/views/newfromtemplate.html +++ b/assets/app/views/newfromtemplate.html @@ -3,19 +3,11 @@
-
+
-
-

{{ template.metadata.name }}

- -
{{ template | description }}
-
-

Source

{{ templateUrl }} -
+
+

Images

    @@ -31,8 +23,8 @@

    Images

    Items will be created in the {{ projectDisplayName() }} project.
- - Cancel + + Cancel
diff --git a/assets/test/karma.conf.js b/assets/test/karma.conf.js index 30b55a763fc6..66886bc5434e 100644 --- a/assets/test/karma.conf.js +++ b/assets/test/karma.conf.js @@ -45,6 +45,7 @@ module.exports = function(config) { "bower_components/kubernetes-label-selector/labelFilter.js", 'app/scripts/**/*.js', //'test/mock/**/*.js', + 'test/spec/spec-helper.js', 'test/spec/**/*.js' ], diff --git a/assets/test/spec/controllers/create/createFromImageSpec.js b/assets/test/spec/controllers/create/createFromImageSpec.js new file mode 100644 index 000000000000..3c9a77fa2fa3 --- /dev/null +++ b/assets/test/spec/controllers/create/createFromImageSpec.js @@ -0,0 +1,43 @@ +"use strict"; + +describe("CreateFromImageController", function(){ + var controller; + var $scope = { + name: "apPname", + projectName: "aProjectName" + }; + var $routeParams = { + imageName: "anImageName", + imageTag: "latest", + namespace: "aNamespace" + }; + var DataService = {}; + var Navigate = {}; + + + beforeEach(function(){ + inject(function(_$controller_){ + // The injector unwraps the underscores (_) from around the parameter names when matching + controller = _$controller_("CreateFromImageController", { + $scope: $scope, + $routeParams: $routeParams, + DataService: { + get: function(kind){ + if(kind === 'imageRepositories'){ + return {}; + } + return {}; + } + }, + Navigate: { + toErrorPage: function(message){} + }, + NameGenerator: { + suggestFromSourceUrl: function(sourceUrl, kinds, namespace){ + return "aName"; + } + } + }); + }); + }); +}); \ No newline at end of file diff --git a/assets/test/spec/directives/oscKeyValuesSpec.js b/assets/test/spec/directives/oscKeyValuesSpec.js new file mode 100644 index 000000000000..7031930835f8 --- /dev/null +++ b/assets/test/spec/directives/oscKeyValuesSpec.js @@ -0,0 +1,124 @@ +"use strict"; + +describe("KeyValuesEntryController", function(){ + var scope, controller; + beforeEach(function(){ + scope = { + value: "foo" + }; + inject(function(_$controller_){ + // The injector unwraps the underscores (_) from around the parameter names when matching + controller = _$controller_("KeyValuesEntryController", {$scope: scope}); + }); + }); + + describe("#edit", function(){ + it("should copy the original value", function(){ + scope.edit(); + expect(scope.value).toEqual(scope.originalValue); + expect(scope.editing).toEqual(true); + }); + }); + + describe("#cancel", function(){ + it("should reset value to the original value", function(){ + scope.originalValue = "bar"; + scope.cancel(); + expect(scope.value).toEqual("bar"); + expect(scope.editing).toEqual(false); + }); + }); + + describe("#update", function(){ + var entries = { foo: "abc"}; + it("should update the entries for the key when the value is not empty", function(){ + scope.update("foo", "bar", entries); + expect(entries["foo"]).toEqual("bar"); + expect(scope.editing).toEqual(false); + }); + }); +}); + +describe("KeyValuesController", function(){ + var scope, controller; + + beforeEach(function(){ + scope = { + entries: { "foo": "bar"}, + form: { + $setPristine: function(){}, + $setUntouched: function(){}, + $setValidity: function(){}, + }, + readonlyKeys: "" + }; + inject(function(_$controller_){ + // The injector unwraps the underscores (_) from around the parameter names when matching + controller = _$controller_("KeyValuesController", {$scope: scope}); + }); + + }); + + describe("#allowDelete", function(){ + + it("should when the deletePolicy equals always", function(){ + expect(scope.allowDelete("foo")).toBe(true); + }); + + it("should not when the deletePolicy equals never", function(){ + scope.deletePolicy = "never"; + expect(scope.allowDelete("foo")).toBe(false); + }); + + it("should when the deletePolicy equals added and the entry was not originally in entries", function(){ + scope.deletePolicy = "added"; + scope.key = "abc"; + scope.value = "def"; + scope.addEntry(); + expect(scope.allowDelete("abc")).toBe(true); + }); + + it("should not when the deletePolicy equals added and the entry was originally in entries", function(){ + scope.deletePolicy = "added"; + expect(scope.allowDelete("foo")).toBe(false); + }); + + }); + + describe("#addEntry", function(){ + it("should not add the entry if the key is in the readonly list", function(){ + scope.readonlyKeys = "abc"; + scope.key = "abc"; + scope.value = "xyz"; + scope.addEntry(); + expect(scope.entries["abc"]).toBe(undefined); + expect(scope.key).toEqual("abc"); + expect(scope.value).toEqual("xyz"); + }); + + it("should add the key/value to the scope", function(){ + scope.key = "foo"; + scope.value = "bar"; + scope.addEntry(); + scope.key = "abc"; + scope.value = "def"; + scope.addEntry(); + expect(scope.entries["foo"]).toEqual("bar"); + expect(scope.entries["abc"]).toEqual("def"); + expect(scope.key).toEqual(null); + expect(scope.value).toEqual(null); + }); + + }); + + describe("#deleteEntry", function(){ + //TODO add test for nonrecognized key? + + it("should delete the key/value from the scope", function(){ + scope.deleteEntry("foo"); + expect(scope.entries["foo"]).toBe(undefined); + }); + }); +}); + + diff --git a/assets/test/spec/directives/oscResourceNameValidatorSpec.js b/assets/test/spec/directives/oscResourceNameValidatorSpec.js new file mode 100644 index 000000000000..5ff5204ba777 --- /dev/null +++ b/assets/test/spec/directives/oscResourceNameValidatorSpec.js @@ -0,0 +1,63 @@ +"use strict"; + +describe("oscResourceNameValidator", function(){ + + var $scope, form; + beforeEach(function(){ + inject(function($compile, $rootScope){ + $scope = $rootScope; + var element = angular.element( + '
' + + '' + + '
' + ); + $scope.model = { name: null }; + $compile(element)($scope); + form = $scope.form; + }); + }); + + it("should disallow a null name", function(){ + form.name.$setViewValue(null); + expect(form.name.$valid).toBe(false); + }); + + it("should disallow an empty name", function(){ + form.name.$setViewValue(""); + expect(form.name.$valid).toBe(false); + }); + + it("should disallow a blank name", function(){ + form.name.$setViewValue(" "); + expect(form.name.$valid).toBe(false); + }); + + it("should disallow a name with a blank", function(){ + form.name.$setViewValue("foo bar"); + expect(form.name.$valid).toBe(false); + }); + + it("should disallow a name with starting with a .", function(){ + form.name.$setViewValue(".foobar"); + expect(form.name.$valid).toBe(false); + }); + + it("should disallow a name that is too long", function(){ + form.name.$setViewValue("abcdefghijklmnopqrstuvwxy"); + expect(form.name.$valid).toBe(false); + }); + + + + it("should allow a name with a dash", function(){ + form.name.$setViewValue("foo-bar"); + expect(form.name.$valid).toBe(true); + }); + + it("should allow a name with a dot", function(){ + form.name.$setViewValue("foo99.bar"); + expect(form.name.$valid).toBe(true); + }); + +}); + diff --git a/assets/test/spec/filters/defaultIfEmptySpec.js b/assets/test/spec/filters/defaultIfEmptySpec.js new file mode 100644 index 000000000000..ca3b1fe48179 --- /dev/null +++ b/assets/test/spec/filters/defaultIfEmptySpec.js @@ -0,0 +1,32 @@ +"use strict"; + +describe("defaultIfBlankFilter", function(){ + var defaultIfBlank; + + beforeEach( + inject(function(defaultIfBlankFilter){ + defaultIfBlank = defaultIfBlankFilter; + }) + ); + + it("should return the value if a non-string", function(){ + expect(defaultIfBlank(1, "foo")).toBe("1"); + }); + + it("should return the value if a non empty string", function(){ + expect(defaultIfBlank("theValue", "foo")).toBe("theValue"); + }); + + it("should return the default if a null string", function(){ + expect(defaultIfBlank(null, "foo")).toBe("foo"); + }); + + it("should return the default if an empty string", function(){ + expect(defaultIfBlank("", "foo")).toBe("foo"); + }); + + it("should return the default if a blank string", function(){ + expect(defaultIfBlank(" ", "foo")).toBe("foo"); + }); + +}); \ No newline at end of file diff --git a/assets/test/spec/filters/httpHttpsSpec.js b/assets/test/spec/filters/httpHttpsSpec.js new file mode 100644 index 000000000000..fbef64267339 --- /dev/null +++ b/assets/test/spec/filters/httpHttpsSpec.js @@ -0,0 +1,18 @@ +"use strict"; + +describe("httpHttpsFilter", function(){ + + it("should return https:// when true", function(){ + inject(function(httpHttpsFilter){ + expect(httpHttpsFilter(true)).toBe("https://"); + }) + }); + + it("should return http:// when false", function(){ + inject(function(httpHttpsFilter){ + expect(httpHttpsFilter(false)).toBe("http://"); + }) + }); +}); + + diff --git a/assets/test/spec/filters/valuesInSpec.js b/assets/test/spec/filters/valuesInSpec.js new file mode 100644 index 000000000000..0a2bd82a7862 --- /dev/null +++ b/assets/test/spec/filters/valuesInSpec.js @@ -0,0 +1,22 @@ +"use strict"; + +describe("valuesInFilter", function(){ + var filter; + var entries = { + foo: "bar", + abc: "xyz", + another: "value" + }; + beforeEach(function(){ + inject(function(valuesInFilter){ + filter = valuesInFilter; + }); + }); + + it("should return a subset of the entries", function(){ + var results = filter(entries,"foo,another"); + delete entries["abc"]; + expect(results).toEqual(entries); + }); +}); + \ No newline at end of file diff --git a/assets/test/spec/filters/valuesNotInSpec.js b/assets/test/spec/filters/valuesNotInSpec.js new file mode 100644 index 000000000000..fa1a5600308e --- /dev/null +++ b/assets/test/spec/filters/valuesNotInSpec.js @@ -0,0 +1,23 @@ +"use strict"; + +describe("valuesNotInFilter", function(){ + var filter; + var entries = { + foo: "bar", + abc: "xyz", + another: "value" + }; + beforeEach(function(){ + inject(function(valuesNotInFilter){ + filter = valuesNotInFilter; + }); + }); + + it("should return a subset of the entries", function(){ + var results = filter(entries,"foo,another"); + delete entries["foo"]; + delete entries["another"]; + expect(results).toEqual(entries); + }); +}); + \ No newline at end of file diff --git a/assets/test/spec/filters/yesNoSpec.js b/assets/test/spec/filters/yesNoSpec.js new file mode 100644 index 000000000000..6dde32f696d8 --- /dev/null +++ b/assets/test/spec/filters/yesNoSpec.js @@ -0,0 +1,18 @@ +"use strict"; + +describe("yesNoFilter", function(){ + + it("should return Yes when true", function(){ + inject(function(yesNoFilter){ + expect(yesNoFilter(true)).toBe("Yes"); + }) + }); + + it("should return No when false", function(){ + inject(function(yesNoFilter){ + expect(yesNoFilter(false)).toBe("No"); + }) + }); +}); + + diff --git a/assets/test/spec/services/applicationGeneratorSpec.js b/assets/test/spec/services/applicationGeneratorSpec.js new file mode 100644 index 000000000000..579d34844e14 --- /dev/null +++ b/assets/test/spec/services/applicationGeneratorSpec.js @@ -0,0 +1,353 @@ +"use strict"; + +describe("ApplicationGenerator", function(){ + var ApplicationGenerator; + var input; + + beforeEach(function(){ + module('openshiftConsole', function($provide){ + $provide.value("DataService",{ + osApiVersion: "v1beta1", + k8sApiVersion: "v1beta3" + }); + }); + + inject(function(_ApplicationGenerator_){ + ApplicationGenerator = _ApplicationGenerator_; + ApplicationGenerator._generateSecret = function(){ + return "secret101"; + }; + }); + + input = { + name: "ruby-hello-world", + routing: true, + buildConfig: { + sourceUrl: "https://github.com/openshift/ruby-hello-world.git", + buildOnSourceChange: true, + buildOnImageChange: true + }, + deploymentConfig: { + deployOnConfigChange: true, + deployOnNewImage: true, + envVars: { + "ADMIN_USERNAME" : "adminEME", + "ADMIN_PASSWORD" : "xFSkebip", + "MYSQL_ROOT_PASSWORD" : "qX6JGmjX", + "MYSQL_DATABASE" : "root" + } + }, + labels : { + foo: "bar", + abc: "xyz" + }, + scaling: { + replicas: 1 + }, + imageName: "origin-ruby-sample", + imageTag: "latest", + imageRepo: { + "kind": "ImageRepository", + "apiVersion": "v1beta1", + "metadata": { + "name": "origin-ruby-sample", + "namespace": "test", + "selfLink": "/osapi/v1beta1/imageRepositories/origin-ruby-sample?namespace=test", + "uid": "ea1d67fc-c358-11e4-90e6-080027c5bfa9", + "resourceVersion": "150", + "creationTimestamp": "2015-03-05T16:58:58Z" + }, + "tags": { + "latest": "ea15999fd97b2f1bafffd615697ef8c14abdfd9ab17ff4ed67cf5857fec8d6c0" + }, + "status": { + "dockerImageRepository": "172.30.17.58:5000/test/origin-ruby-sample" + } + }, + image: { + "kind" : "Image", + "metadata" : { + "name" : "ea15999fd97b2f1bafffd615697ef8c14abdfd9ab17ff4ed67cf5857fec8d6c0" + }, + "dockerImageMetadata" : { + "ContainerConfig" : { + "ExposedPorts": { + "443/tcp": {}, + "80/tcp": {} + }, + "Env": [ + "STI_SCRIPTS_URL" + ] + } + } + } + }; + }); + + describe("#_generateService", function(){ + it("should generate a headless service when no ports are exposed", function(){ + var copy = angular.copy(input); + copy.image.dockerImageMetadata.ContainerConfig.ExposedPorts = {}; + var service = ApplicationGenerator._generateService(copy, "theServiceName", "None"); + expect(service).toEqual( + { + "kind": "Service", + "apiVersion": "v1beta3", + "metadata": { + "name": "theServiceName", + "labels" : { + "foo" : "bar", + "abc" : "xyz" } + }, + "spec": { + "portalIP" : "None", + "selector": { + "deploymentconfig": "ruby-hello-world" + } + } + }); + }); + }); + + describe("#_generateRoute", function(){ + + it("should generate nothing if routing is not required", function(){ + input.routing = false; + expect(ApplicationGenerator._generateRoute(input, input.name, "theServiceName")).toBe(null); + }); + + it("should generate an unsecure Route when routing is required", function(){ + var route = ApplicationGenerator._generateRoute(input, input.name, "theServiceName"); + expect(route).toEqual({ + kind: "Route", + apiVersion: 'v1beta1', + metadata: { + name: "ruby-hello-world", + labels : { + "foo" : "bar", + "abc" : "xyz" + } + }, + serviceName: "theServiceName", + tls: { + termination: "unsecure" + } + }); + }); + }); + + describe("generating applications from image that includes source", function(){ + var resources; + beforeEach(function(){ + resources = ApplicationGenerator.generate(input); + }); + + it("should generate a BuildConfig for the source", function(){ + expect(resources.buildConfig).toEqual( + { + "apiVersion": "v1beta1", + "kind": "BuildConfig", + "metadata": { + "name": "ruby-hello-world", + labels : { + "foo" : "bar", + "abc" : "xyz", + "name": "ruby-hello-world", + "generatedby": "OpenShiftWebConsole" + } + }, + "parameters": { + "output": { + "to": { + "name": "ruby-hello-world" + } + }, + "source": { + "git": { + "ref": "master", + "uri": "https://github.com/openshift/ruby-hello-world.git" + }, + "type": "Git" + }, + "strategy": { + "type": "STI", + "stiStrategy" : { + "image" : "172.30.17.58:5000/test/origin-ruby-sample:latest" + } + } + }, + "triggers": [ + { + "generic": { + "secret": "secret101" + }, + "type": "generic" + }, + { + "github": { + "secret": "secret101" + }, + "type": "github" + }, + { + "imageChange" : { + "image" : "172.30.17.58:5000/test/origin-ruby-sample:latest", + "from" : { + "name" : "origin-ruby-sample" + }, + "tag" : "latest" + }, + "type" : "imageChange" + } + ] + + } + ); + }); + + it("should generate an ImageRepository for the build output", function(){ + expect(resources.imageRepo).toEqual( + { + "apiVersion": "v1beta1", + "kind": "ImageRepository", + "metadata": { + "name": "ruby-hello-world", + labels : { + "foo" : "bar", + "abc" : "xyz", + "name": "ruby-hello-world", + "generatedby": "OpenShiftWebConsole" + } + } + } + ); + }); + + it("should generate a Service for the build output", function(){ + expect(resources.service).toEqual( + { + "kind": "Service", + "apiVersion": "v1beta3", + "metadata": { + "name": "ruby-hello-world", + "labels" : { + "foo" : "bar", + "abc" : "xyz", + "name": "ruby-hello-world", + "generatedby": "OpenShiftWebConsole" + } + }, + "spec": { + "port": 80, + "containerPort" : 80, + "protocol": "tcp", + "selector": { + "deploymentconfig": "ruby-hello-world" + } + } + } + ); + }); + + it("should generate a DeploymentConfig for the BuildConfig output image", function(){ + var resources = ApplicationGenerator.generate(input); + expect(resources.deploymentConfig).toEqual( + { + "apiVersion": "v1beta1", + "kind": "DeploymentConfig", + "metadata": { + "name": "ruby-hello-world", + "labels": { + "foo" : "bar", + "abc" : "xyz", + "name": "ruby-hello-world", + "generatedby" : "OpenShiftWebConsole", + "deploymentconfig": "ruby-hello-world" + } + }, + "template": { + "controllerTemplate": { + "podTemplate": { + "desiredState": { + "manifest": { + "containers": [ + { + "image": "ruby-hello-world:latest", + "name": "ruby-hello-world", + "ports": [ + { + "containerPort": 443, + "name": "ruby-hello-world-tcp-443", + "protocol": "tcp" + }, + { + "containerPort": 80, + "name": "ruby-hello-world-tcp-80", + "protocol": "tcp" + } + ], + "env" : [ + { + "name": "ADMIN_USERNAME", + "value": "adminEME" + }, + { + "name": "ADMIN_PASSWORD", + "value": "xFSkebip" + }, + { + "name": "MYSQL_ROOT_PASSWORD", + "value": "qX6JGmjX" + }, + { + "name": "MYSQL_DATABASE", + "value": "root" + } + ] + } + ], + "version": "v1beta3" + } + }, + "labels": { + "foo" : "bar", + "abc" : "xyz", + "name": "ruby-hello-world", + "generatedby" : "OpenShiftWebConsole", + "deploymentconfig": "ruby-hello-world" + } + }, + "replicaSelector": { + "deploymentconfig": "ruby-hello-world" + }, + "replicas": 1 + }, + "strategy": { + "type": "Recreate" + } + }, + "triggers": [ + { + "type": "ImageChange", + "imageChangeParams": { + "automatic": true, + "containerNames": [ + "ruby-hello-world" + ], + "from": { + "name": "ruby-hello-world" + }, + "tag": "latest" + } + }, + { + "type": "ConfigChange" + } + ] + } + ); + }); + + }); + +}); \ No newline at end of file diff --git a/assets/test/spec/services/nameGeneratorSpec.js b/assets/test/spec/services/nameGeneratorSpec.js new file mode 100644 index 000000000000..d6538566b2c8 --- /dev/null +++ b/assets/test/spec/services/nameGeneratorSpec.js @@ -0,0 +1,31 @@ +"use strict"; + +describe("NameGenerator", function(){ + var NameGenerator; + + beforeEach(function($provide){ + + inject(function(_NameGenerator_){ + NameGenerator = _NameGenerator_; + }); + }); + + describe("#suggestFromSourceUrl", function(){ + + var sourceUrl = "git@github.com:openshift/ruby-hello-world.git"; + + it("should suggest a name based on git source url ending with 'git'", function(){ + var result = NameGenerator.suggestFromSourceUrl(sourceUrl); + expect(result).toEqual("ruby-hello-world"); + }); + + it("should suggest a name based on git source url not ending with 'git'", function(){ + + sourceUrl = "git@github.com:openshift/ruby-hello-world"; + var result = NameGenerator.suggestFromSourceUrl(sourceUrl); + expect(result).toEqual("ruby-hello-world"); + }); + + }); + +}); diff --git a/assets/test/spec/spec-helper.js b/assets/test/spec/spec-helper.js new file mode 100644 index 000000000000..93587427058e --- /dev/null +++ b/assets/test/spec/spec-helper.js @@ -0,0 +1,44 @@ +"use strict"; +// Angular is refusing to recognize the HawtioNav stuff +// when testing even though its being loaded + beforeEach(module(function ($provide) { + $provide.provider("HawtioNavBuilder", function() { + function Mocked() {} + this.create = function() {return this;}; + this.id = function() {return this;}; + this.title = function() {return this;}; + this.template = function() {return this;}; + this.isSelected = function() {return this;}; + this.href = function() {return this;}; + this.page = function() {return this;}; + this.subPath = function() {return this;}; + this.build = function() {return this;}; + this.join = function() {return "";}; + this.$get = function() {return new Mocked();}; + }); + + $provide.factory("HawtioNav", function(){ + return {add: function() {}}; + }); + +})); + +beforeEach(function(){ + module('openshiftConsole', function($provide){ + $provide.factory("DataService", function(){ + return {}; + }); + }); +}); + +// Make sure a base location exists in the generated test html + if (!$('head base').length) { + $('head').append($('')); + } + + angular.module('openshiftConsole').config(function(AuthServiceProvider) { + AuthServiceProvider.UserStore('MemoryUserStore'); + }); + + //load the module +beforeEach(module('openshiftConsole')); diff --git a/examples/image-streams/image-streams.json b/examples/image-streams/image-streams.json index 91ed65d8bf06..fb2d06cee89e 100644 --- a/examples/image-streams/image-streams.json +++ b/examples/image-streams/image-streams.json @@ -9,7 +9,18 @@ "name": "ruby-20-centos7" }, "spec": { - "dockerImageRepository": "openshift/ruby-20-centos7" + "dockerImageRepository": "openshift/ruby-20-centos7", + "tags": [ + { + "name": "latest", + "annotations": { + "description": "Build and run Ruby 2.0 applications", + "iconClass": "icon-ruby", + "tags": "builder,ruby", + "version": "2.0" + } + } + ] } }, { @@ -19,7 +30,18 @@ "name": "nodejs-010-centos7" }, "spec": { - "dockerImageRepository": "openshift/nodejs-010-centos7" + "dockerImageRepository": "openshift/nodejs-010-centos7", + "tags": [ + { + "name": "latest", + "annotations": { + "description" : "Build and run NodeJS 0.10 applications", + "iconClass" : "icon-nodejs", + "tags" : "builder,nodejs", + "version" : "0.10" + } + } + ] } }, { @@ -29,7 +51,18 @@ "name": "wildfly-8-centos" }, "spec": { - "dockerImageRepository": "openshift/wildfly-8-centos" + "dockerImageRepository": "openshift/wildfly-8-centos", + "tags": [ + { + "name": "latest", + "annotations": { + "description" : "Build and run Java applications on Wildfly 8", + "iconClass" : "icon-wildfly", + "tags" : "builder,wildfly,java", + "version" : "8" + } + } + ] } }, { @@ -63,4 +96,4 @@ } } ] -} +} \ No newline at end of file diff --git a/examples/sample-app/application-template-stibuild.json b/examples/sample-app/application-template-stibuild.json index 70dc657e4870..e4bccde8a268 100644 --- a/examples/sample-app/application-template-stibuild.json +++ b/examples/sample-app/application-template-stibuild.json @@ -245,7 +245,9 @@ "kind": "Template", "metadata": { "annotations": { - "description": "This example shows how to create a simple ruby application in openshift origin v3" + "description": "This example shows how to create a simple ruby application in openshift origin v3", + "tags": "instant-app,ruby,mysql", + "iconClass" : "icon-ruby" }, "name": "ruby-helloworld-sample" }, diff --git a/pkg/assets/bindata.go b/pkg/assets/bindata.go index 04bf25824202..4233718d1ac5 100644 --- a/pkg/assets/bindata.go +++ b/pkg/assets/bindata.go @@ -10908,6 +10908,7 @@ you can use the generic selector below, but it's slower: .tile.tile-status { background-color: #e6ecf1; border-top: 5px solid #bfcedb; + margin-top: 40px; } .tile-click { cursor: pointer; @@ -11130,6 +11131,23 @@ ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:b content: "\e604"; background-color: transparent; } +.btn-file { + position: relative; + overflow: hidden; +} +.btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + cursor: inherit; + display: block; +} .pod { padding: 10px; border-radius: 10px; @@ -11811,6 +11829,7 @@ body { } .console-os .container-main { background-color: #f8f8f8; + padding-bottom: 80px; -webkit-flex: 1; -moz-flex: 1; -ms-flex: 1; @@ -11818,6 +11837,7 @@ body { } .console-os #content-wrap > .container { margin-top: 35px; + margin-bottom: 80px; } .console-os #content-wrap > .container h1 { margin-top: 10px; @@ -11917,47 +11937,61 @@ body { text-align: left; font-weight: normal; } -.create-from-template .template-name { +.create-from-template .template-name, +.create-from-image .template-name { text-align: right; } -.create-from-template .template-name span.fa { +.create-from-template .template-name span.fa, +.create-from-image .template-name span.fa { font-size: 40px; } @media (min-width: 768px) { - .create-from-template .template-name span.fa { + .create-from-template .template-name span.fa, + .create-from-image .template-name span.fa { font-size: 100px; } } -.create-from-template span.fa.visible-xs-inline { +.create-from-template span.fa.visible-xs-inline, +.create-from-image span.fa.visible-xs-inline { margin-right: 10px; } -.flow { +.create-from-template .flow, +.create-from-image .flow { + border-top: 1px solid rgba(0, 0, 0, 0.15); display: table; + margin-top: 60px; width: 100%; } -.flow > .flow-block { +.create-from-template .flow > .flow-block, +.create-from-image .flow > .flow-block { display: inline-block; } -.flow > .flow-block.right { +.create-from-template .flow > .flow-block.right, +.create-from-image .flow > .flow-block.right { font-size: 11px; font-weight: normal; } -.flow > .flow-block .action { +.create-from-template .flow > .flow-block .action, +.create-from-image .flow > .flow-block .action { font-size: 11px; font-weight: normal; } -.flow > .flow-block > ul.list-inline { +.create-from-template .flow > .flow-block > ul.list-inline, +.create-from-image .flow > .flow-block > ul.list-inline { margin-bottom: 0; } -.flow > .flow-block > ul.list-inline > li { +.create-from-template .flow > .flow-block > ul.list-inline > li, +.create-from-image .flow > .flow-block > ul.list-inline > li { font-size: 11px; text-align: left; } @media (min-width: 767px) { - .flow > .flow-block { + .create-from-template .flow > .flow-block, + .create-from-image .flow > .flow-block { display: table-cell; } - .flow > .flow-block.right { + .create-from-template .flow > .flow-block.right, + .create-from-image .flow > .flow-block.right { text-align: right; } } @@ -11987,15 +12021,18 @@ body { .modal.modal-create .modal-content .modal-header { background-color: transparent; } -.modal.modal-create .modal-content .modal-body .template-icon { +.modal.modal-create .modal-content .modal-body .template-icon, +.modal.modal-create .modal-content .modal-body .image-icon { text-align: center; } -.modal.modal-create .modal-content .modal-body .template-icon { +.modal.modal-create .modal-content .modal-body .template-icon, +.modal.modal-create .modal-content .modal-body .image-icon { font-size: 80px; line-height: 80px; } @media (min-width: 768px) { - .modal.modal-create .modal-content .modal-body .template-icon { + .modal.modal-create .modal-content .modal-body .template-icon, + .modal.modal-create .modal-content .modal-body .image-icon { font-size: 130px; line-height: 130px; } @@ -12012,7 +12049,8 @@ body { margin-left: 3px; } .action-inline { - margin-left: 15px; + margin-left: 5px; + font-size: 11px; } .action-inline i.pficon, .action-inline i.fa { @@ -12032,13 +12070,22 @@ code { white-space: normal; } .gutter-top-bottom { - padding: 15px 0; + padding: 20px 0; +} +.gutter-top-bottom.gutter-top-bottom-2x { + padding: 40px 0; } .gutter-top { - padding-top: 15px; + padding-top: 20px; +} +.gutter-top.gutter-top-2x { + padding-top: 40px; } .gutter-bottom { - padding-bottom: 15px; + padding-bottom: 20px; +} +.gutter-bottom.gutter-bottom-2x { + padding-bottom: 40px; } select:invalid { box-shadow: none; @@ -12078,6 +12125,30 @@ select:invalid { background-color: #f1f1f1; color: #666; } +.input-number { + width: 60px; +} +.fade { + opacity: 0; + -webkit-transition: opacity 0.2s ease 0s; + transition: opacity 0.2s ease 0s; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.1s ease; + transition: height 0.1s ease; +} `) func css_main_css() ([]byte, error) { @@ -12870,10 +12941,16 @@ templateUrl:"views/images.html" templateUrl:"views/pods.html" }).when("/project/:project/browse/services", { templateUrl:"views/services.html" -}).when("/project/:project/catalog", { -templateUrl:"views/catalog.html" +}).when("/project/:project/catalog/templates", { +templateUrl:"views/catalog/templates.html" +}).when("/project/:project/catalog/images", { +templateUrl:"views/catalog/images.html" +}).when("/project/:project/create", { +templateUrl:"views/create.html" }).when("/project/:project/create/fromtemplate", { templateUrl:"views/newfromtemplate.html" +}).when("/project/:project/create/fromimage", { +templateUrl:"views/create/fromimage.html" }).when("/oauth", { templateUrl:"views/util/oauth.html", controller:"OAuthController" @@ -13172,7 +13249,7 @@ this._listCallbacksMap = {}, this._watchCallbacksMap = {}, this._watchOperationM var a = this; c.$on("$routeChangeStart", function() { a._watchWebsocketRetriesMap = {}; -}); +}), this.osApiVersion = "v1beta1", this.k8sApiVersion = "v1beta3"; } h.prototype.by = function(a) { if ("metadata.name" === a) return this._data; @@ -13249,7 +13326,7 @@ h.push(a), j--, e(); }); }), f.promise; }, i.prototype.get = function(b, e, g, h) { -h = h || {}; +void 0 !== this._objectType(b) && (b = this._objectType(b)), h = h || {}; var i = !!h.force; delete h.force; var j = d.defer(), k = this._data(b, g); @@ -13427,6 +13504,7 @@ buildConfigHooks:e.openshift, deploymentConfigs:e.openshift, images:e.openshift, imageRepositories:e.openshift, +imageRepositoryTags:e.openshift, imageStreams:e.openshift, imageStreamImages:e.openshift, imageStreamTags:e.openshift, @@ -13513,6 +13591,186 @@ namespace:a.metadata.name }); }) :e.resolve(null), e.promise; }, new i(); +} ]), angular.module("openshiftConsole").service("ApplicationGenerator", [ "DataService", function(a) { +var b = a.osApiVersion, c = a.k8sApiVersion, d = {}; +return d._generateSecret = function() { +function a() { +return Math.floor(65536 * (1 + Math.random())).toString(16).substring(1); +} +return a() + a() + a() + a(); +}, d._getFirstPort = function(a) { +var b = "None"; +return a.forEach(function(a) { +"None" === b ? b = a :a.containerPort < b.containerPort && (b = a); +}), b; +}, d.generate = function(a) { +var b = []; +angular.forEach(a.image.dockerImageMetadata.ContainerConfig.ExposedPorts, function(c, d) { +var e = d.split("/"); +1 === e.length && e.push("tcp"), b.push({ +containerPort:parseInt(e[0]), +name:a.name + "-" + e[1] + "-" + e[0], +protocol:e[1] +}); +}), a.labels.name = a.name, a.labels.generatedby = "OpenShiftWebConsole"; +var c; +null !== a.buildConfig.sourceUrl && (c = { +name:a.name, +tag:"latest", +toString:function() { +return this.name + ":" + this.tag; +} +}); +var e = { +imageRepo:d._generateImageRepo(a), +buildConfig:d._generateBuildConfig(a, c, a.labels), +deploymentConfig:d._generateDeploymentConfig(a, c, b, a.labels), +service:d._generateService(a, a.name, d._getFirstPort(b)) +}; +return e.route = d._generateRoute(a, a.name, e.service.metadata.name), e; +}, d._generateRoute = function(a, c, d) { +return a.routing ? { +kind:"Route", +apiVersion:b, +metadata:{ +name:c, +labels:a.labels +}, +serviceName:d, +tls:{ +termination:"unsecure" +} +} :null; +}, d._generateDeploymentConfig = function(a, d, e, f) { +var g = []; +angular.forEach(a.deploymentConfig.envVars, function(a, b) { +g.push({ +name:b, +value:a +}); +}), f = angular.copy(f), f.deploymentconfig = a.name; +var h = { +apiVersion:b, +kind:"DeploymentConfig", +metadata:{ +name:a.name, +labels:f +}, +template:{ +controllerTemplate:{ +podTemplate:{ +desiredState:{ +manifest:{ +containers:[ { +image:d.toString(), +name:a.name, +ports:e, +env:g +} ], +version:c +} +}, +labels:f +}, +replicaSelector:{ +deploymentconfig:a.name +}, +replicas:a.scaling.replicas +}, +strategy:{ +type:"Recreate" +} +}, +triggers:[] +}; +return a.deploymentConfig.deployOnNewImage && h.triggers.push({ +type:"ImageChange", +imageChangeParams:{ +automatic:!0, +containerNames:[ a.name ], +from:{ +name:d.name +}, +tag:d.tag +} +}), a.deploymentConfig.deployOnConfigChange && h.triggers.push({ +type:"ConfigChange" +}), h; +}, d._generateBuildConfig = function(a, c, e) { +var f = a.imageRepo.status.dockerImageRepository + ":" + a.imageTag, g = [ { +generic:{ +secret:d._generateSecret() +}, +type:"generic" +} ]; +return a.buildConfig.buildOnSourceChange && g.push({ +github:{ +secret:d._generateSecret() +}, +type:"github" +}), a.buildConfig.buildOnImageChange && g.push({ +imageChange:{ +image:f, +from:{ +name:a.imageName +}, +tag:a.imageTag +}, +type:"imageChange" +}), { +apiVersion:b, +kind:"BuildConfig", +metadata:{ +name:a.name, +labels:e +}, +parameters:{ +output:{ +to:{ +name:c.name +} +}, +source:{ +git:{ +ref:"master", +uri:a.buildConfig.sourceUrl +}, +type:"Git" +}, +strategy:{ +type:"STI", +stiStrategy:{ +image:f +} +} +}, +triggers:g +}; +}, d._generateImageRepo = function(a) { +return { +apiVersion:b, +kind:"ImageRepository", +metadata:{ +name:a.name, +labels:a.labels +} +}; +}, d._generateService = function(a, b, d) { +var e = { +kind:"Service", +apiVersion:c, +metadata:{ +name:b, +labels:a.labels +}, +spec:{ +selector:{ +deploymentconfig:a.name +} +} +}; +return "None" === d ? e.spec.portalIP = "None" :(e.spec.port = d.containerPort, e.spec.containerPort = d.containerPort, e.spec.protocol = d.protocol), e; +}, d; } ]), angular.module("openshiftConsole").provider("RedirectLoginService", function() { var a = "", b = "", c = ""; this.OAuthClientID = function(b) { @@ -13589,6 +13847,29 @@ return f["delete"]("oAuthAccessTokens", e, {}, g); } }; } ]; +}), angular.module("openshiftConsole").service("Navigate", [ "$location", function(a) { +return { +toErrorPage:function(b, c) { +var d = URI("error").query({ +error_description:b, +error:c +}).toString(); +a.url(d); +}, +toProjectOverview:function(b) { +a.path(this.projectOverviewURL(b)); +}, +projectOverviewURL:function(a) { +return "project/" + encodeURIComponent(a) + "/overview"; +} +}; +} ]), angular.module("openshiftConsole").service("NameGenerator", function() { +return { +suggestFromSourceUrl:function(a) { +var b = a.substr(a.lastIndexOf("/") + 1, a.length), c = b.lastIndexOf("."); +return -1 !== c && (b = b.substr(0, c)), b; +} +}; }), angular.module("openshiftConsole").factory("TaskList", [ "$interval", function(a) { function b() { this.tasks = []; @@ -13939,38 +14220,120 @@ a.services = b.select(a.unfilteredServices), h(); }), a.$on("$destroy", function() { b.unwatchAll(f); }); +} ]), angular.module("openshiftConsole").controller("CreateFromImageController", [ "$scope", "Logger", "$q", "$routeParams", "DataService", "Navigate", "NameGenerator", "ApplicationGenerator", "TaskList", function(a, b, c, d, e, f, g, h, i) { +function j(a) { +d.imageName || f.toErrorPage("Cannot create from source: a base image was not specified"), d.imageTag || f.toErrorPage("Cannot create from source: a base image tag was not specified"), d.sourceURL || f.toErrorPage("Cannot create from source: source url was not specified"), a.emptyMessage = "Loading...", a.imageName = d.imageName, a.imageTag = d.imageTag, a.namespace = d.namespace, a.buildConfig = { +sourceUrl:d.sourceURL, +buildOnSourceChange:!0, +buildOnImageChange:!0 +}, a.deploymentConfig = { +deployOnNewImage:!0, +deployOnConfigChange:!0, +envVars:{} +}, a.routing = !0, a.labels = {}, a.scaling = { +replicas:1 +}, e.get("imageStreams", a.imageName, a, { +namespace:a.namespace +}).then(function(b) { +a.imageRepo = b; +var c = a.imageTag; +e.get("imageStreamTags", b.metadata.name + ":" + c, { +namespace:a.namespace +}).then(function(b) { +a.image = b, angular.forEach(b.dockerImageMetadata.ContainerConfig.Env, function(b) { +var c = b.split("="); +a.deploymentConfig.envVars[c[0]] = c[1]; +}); +}, function() { +f.toErrorPage("Cannot create from source: the specified image could not be retrieved."); +}); +}, function() { +f.toErrorPage("Cannot create from source: the specified image could not be retrieved."); +}), a.name = g.suggestFromSourceUrl(a.buildConfig.sourceUrl); +} +j(a); +var k = function(a, b, d) { +function f() { +0 === j && (h.length > 0 ? g.reject(h) :g.resolve(a)); +} +var g = c.defer(), h = [], i = [], j = a.length; +return a.forEach(function(a) { +e.get(a.kind, a.metadata.name, d, { +namespace:b, +errorNotification:!1 +}).then(function(a) { +h.push(a), j--, f(); +}, function(a) { +i.push(a), j--, f(); +}); +}), g.promise; +}, l = function(b) { +var d = { +started:"Creating application " + a.name + " in project " + a.projectName, +success:"Created application " + a.name + " in project " + a.projectName, +failure:"Failed to create " + a.name + " in project " + a.projectName +}, g = {}; +i.add(d, g, function() { +var d = c.defer(); +return e.createList(b, a).then(function(b) { +var c = [], e = !1; +b.failure.length > 0 ? b.failure.forEach(function(a) { +var b = ""; +b = a.data && a.data.details ? a.data.details.kind + " " + a.data.details.id :"object", c.push({ +type:"error", +message:"Cannot create " + b + ". ", +details:a.data.message +}), e = !0; +}) :c.push({ +type:"success", +message:"All resource for application " + a.name + " were created successfully." +}), d.resolve({ +alerts:c, +hasErrors:e +}); +}), d.promise; +}, function(b) { +a.alerts = [ { +type:"error", +message:"An error occurred creating the application.", +details:"Status: " + b.status + ". " + b.data +} ]; +}), f.toProjectOverview(a.projectName); +}, m = function() { +a.nameTaken = !0; +}; +a.createApp = function() { +var c = h.generate(a), d = []; +angular.forEach(c, function(a) { +null !== a && (b.debug("Generated resource definition:", a), d.push(a)); +}), k(d, a.namespace, a).then(l, m); +}; } ]), angular.module("openshiftConsole").controller("NewFromTemplateController", [ "$scope", "$http", "$routeParams", "DataService", "$q", "$location", "TaskList", "$parse", function(a, b, c, d, e, f, g, h) { function i(a) { -var b = URI("error").query({ -error_description:a -}).toString(); -f.url(b); -} -function j(a) { -var b = [], c = m(a); +var b = [], c = l(a); return c && c.forEach(function(a) { b.push(a.image); }), b; } -function k(a) { +function j(a) { var b = [], c = [], d = {}; return a.items.forEach(function(a) { if ("BuildConfig" === a.kind) { -var e = n(a); +var e = m(a); e && b.push({ name:e }); -var f = o(a); +var f = n(a); f && (d[f] = !0); } -"DeploymentConfig" === a.kind && (c = c.concat(j(a))); +"DeploymentConfig" === a.kind && (c = c.concat(i(a))); }), c.forEach(function(a) { d[a] || b.push({ name:a }); }), b; } -function l(a) { +function k(a) { var b = /^helplink\.(.*)\.title$/, c = /^helplink\.(.*)\.url$/, d = {}; for (var e in a.annotations) { var f, g = e.match(b); @@ -13978,7 +14341,7 @@ g ? (f = d[g[1]] || {}, f.title = a.annotations[e], d[g[1]] = f) :(g = e.match(c } return d; } -var m = h("template.controllerTemplate.podTemplate.desiredState.manifest.containers"), n = h("parameters.strategy.stiStrategy.image"), o = h("parameters.output.to.name || parameters.output.DockerImageReference"); +var l = h("template.controllerTemplate.podTemplate.desiredState.manifest.containers"), m = h("parameters.strategy.stiStrategy.image"), n = h("parameters.output.to.name || parameters.output.DockerImageReference"); a.projectDisplayName = function() { return this.project && this.project.displayName || this.projectName; }, a.templateDisplayName = function() { @@ -13989,8 +14352,8 @@ var c = { started:"Creating " + a.templateDisplayName() + " in project " + a.projectDisplayName(), success:"Created " + a.templateDisplayName() + " in project " + a.projectDisplayName(), failure:"Failed to create " + a.templateDisplayName() + " in project " + a.projectDisplayName() -}, h = l(a.template); -g.add(c, h, function() { +}, f = k(a.template); +g.add(c, f, function() { var c = e.defer(); return d.createList(b.items, a).then(function(b) { var d = [], e = !1; @@ -14009,7 +14372,7 @@ alerts:d, hasErrors:e }); }), c.promise; -}), f.path("project/" + a.projectName + "/overview"); +}), Navigate.toProjectOverview(a.projectName); }, function(b) { a.alerts = [ { type:"error", @@ -14020,16 +14383,16 @@ details:"Status: " + b.status + ". " + b.data }, a.toggleOptionsExpanded = function() { a.optionsExpanded = !a.optionsExpanded; }; -var p = c.name, q = c.namespace; -return p ? (a.emptyMessage = "Loading...", a.alerts = [], a.projectName = c.project, a.projectPromise = $.Deferred(), d.get("projects", a.projectName, a).then(function(b) { +var o = c.name, p = c.namespace; +return o ? (a.emptyMessage = "Loading...", a.alerts = [], a.projectName = c.project, a.projectPromise = $.Deferred(), d.get("projects", a.projectName, a).then(function(b) { a.project = b, a.projectPromise.resolve(b); -}), void d.get("templates", p, a, { -namespace:q +}), void d.get("templates", o, a, { +namespace:p }).then(function(b) { -a.template = b, a.templateImages = k(b), a.hasParameters = a.template.parameters && a.template.parameters.length > 0, a.optionsExpanded = !1, a.templateUrl = b.metadata.selfLink, b.labels = b.labels || {}; +a.template = b, a.templateImages = j(b), a.hasParameters = a.template.parameters && a.template.parameters.length > 0, a.optionsExpanded = !1, a.templateUrl = b.metadata.selfLink, b.labels = b.labels || {}; }, function() { -i("Cannot create from template: the specified template could not be retrieved."); -})) :void i("Cannot create from template: a template name was not specified."); +Navigate.toErrorPage("Cannot create from template: the specified template could not be retrieved."); +})) :void Navigate.toErrorPage("Cannot create from template: a template name was not specified."); } ]), angular.module("openshiftConsole").controller("LabelsController", [ "$scope", function(a) { a.expanded = !0, a.toggleExpanded = function() { a.expanded = !a.expanded; @@ -14103,7 +14466,7 @@ b.debug("LogoutController"), c.isLoggedIn() ? (b.debug("LogoutController, logged c.isLoggedIn() ? (b.debug("LogoutController, logout failed, still logged in"), a.logoutMessage = 'You could not be logged out. Return to the console.') :d.logout_uri ? (b.debug("LogoutController, logout completed, redirecting to AUTH_CFG.logout_uri", d.logout_uri), window.location.href = d.logout_uri) :(b.debug("LogoutController, logout completed, reloading the page"), window.location.reload(!1)); })) :d.logout_uri ? (b.debug("LogoutController, logout completed, redirecting to AUTH_CFG.logout_uri", d.logout_uri), a.logoutMessage = "Logging out...", window.location.href = d.logout_uri) :(b.debug("LogoutController, not logged in, logout complete"), a.logoutMessage = 'You are logged out. Return to the console.'); } ]), angular.module("openshiftConsole").controller("CatalogController", [ "$scope", "DataService", "$filter", "LabelFilter", "Logger", function(a, b, c, d, e) { -a.projectTemplates = {}, a.openshiftTemplates = {}, a.templatesByTag = {}, a.templates = [], a.instantApps = [], b.list("templates", a, function(b) { +a.projectTemplates = {}, a.openshiftTemplates = {}, a.templatesByTag = {}, a.templates = [], b.list("templates", a, function(b) { a.projectTemplates = b.by("metadata.name"), f(), g(), e.log("project templates", a.projectTemplates); }), b.list("templates", { namespace:"openshift" @@ -14126,6 +14489,64 @@ c = $.trim(c), a.templatesByTag[c] = a.templatesByTag[c] || [], a.templatesByTag } }), e.log("templatesByTag", a.templatesByTag); }; +} ]), angular.module("openshiftConsole").controller("CatalogImagesController", [ "$scope", "DataService", "$filter", "LabelFilter", "imageEnvFilter", "$routeParams", "Logger", function(a, b, c, d, e, f, g) { +a.projectImageRepos = {}, a.openshiftImageRepos = {}, a.builders = [], a.images = [], a.sourceURL = f.builderfor; +var h = function(b) { +angular.forEach(b, function(b) { +b.status && (angular.forEach(b.status.tags, function(c) { +var d = c.tag, e = { +imageRepo:b, +imageRepoTag:d, +name:b.metadata.name + ":" + d +}; +a.images.push(e); +var f = []; +b.spec.tags && angular.forEach(b.spec.tags, function(b) { +b.annotations && b.annotations.tags && (f = b.annotations.tags.split(/\s*,\s*/)), f.indexOf("builder") >= 0 && a.builders.push(e); +}); +}), g.info("builders", a.builders)); +}); +}; +b.list("imageStreams", a, function(b) { +a.projectImageRepos = b.by("metadata.name"), h(a.projectImageRepos, a), g.info("project image repos", a.projectImageRepos); +}), b.list("imageStreams", { +namespace:"openshift" +}, function(b) { +a.openshiftImageRepos = b.by("metadata.name"), h(a.openshiftImageRepos, { +namespace:"openshift" +}), g.info("openshift image repos", a.openshiftImageRepos); +}); +} ]), angular.module("openshiftConsole").controller("CreateController", [ "$scope", "DataService", "$filter", "LabelFilter", "$location", "Logger", function(a, b, c, d, e, f) { +a.projectTemplates = {}, a.openshiftTemplates = {}, a.templatesByTag = {}, a.sourceURLPattern = /^(ftp|http|https|git):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, b.list("templates", a, function(b) { +a.projectTemplates = b.by("metadata.name"), g(), f.info("project templates", a.projectTemplates); +}), b.list("templates", { +namespace:"openshift" +}, function(b) { +a.openshiftTemplates = b.by("metadata.name"), g(), f.info("openshift templates", a.openshiftTemplates); +}); +var g = function() { +a.templatesByTag = {}; +var b = function(b) { +if (b.metadata.annotations && b.metadata.annotations.tags) { +var c = b.metadata.annotations.tags.split(","); +angular.forEach(c, function(c) { +c = $.trim(c), a.templatesByTag[c] = a.templatesByTag[c] || [], a.templatesByTag[c].push(b); +}); +} +}; +angular.forEach(a.projectTemplates, b), angular.forEach(a.openshiftTemplates, b), f.info("templatesByTag", a.templatesByTag); +}; +a.createFromSource = function() { +if (a.from_source_form.$valid) { +var b = URI.expand("/project/{project}/catalog/images{?q*}", { +project:a.projectName, +q:{ +builderfor:a.from_source_url +} +}); +e.url(b.toString()); +} +}; } ]), angular.module("openshiftConsole").directive("relativeTimestamp", function() { return { restrict:"E", @@ -14142,6 +14563,160 @@ timestamp:"=" }, template:'{{timestamp | duration}}' }; +}), angular.module("openshiftConsole").directive("oscFileInput", [ "Logger", function(a) { +return { +restrict:"E", +scope:{ +name:"@", +model:"=", +required:"=" +}, +templateUrl:"views/directives/osc-file-input.html", +link:function(b, c) { +b.supportsFileUpload = window.File && window.FileReader && window.FileList && window.Blob, b.uploadError = !1, $(c).change(function() { +var c = $("input[type=file]", this)[0].files[0], d = new FileReader(); +d.onloadend = function() { +b.$apply(function() { +b.fileName = c.name, b.model = d.result; +}); +}, d.onerror = function(c) { +b.supportsFileUpload = !1, b.uploadError = !0, a.error(c); +}, d.onerror(); +}); +} +}; +} ]), angular.module("openshiftConsole").directive("oscFormSection", function() { +return { +restrict:"E", +transclude:!0, +scope:{ +header:"@", +about:"@", +aboutTitle:"@", +editText:"@", +expand:"@" +}, +templateUrl:"views/directives/osc-form-section.html", +link:function(a, b, c) { +c.editText || (c.editText = "Edit"), a.expand = c.expand ? !0 :!1, a.toggle = function() { +a.expand = !a.expand; +}; +} +}; +}), angular.module("openshiftConsole").directive("oscImageSummary", function() { +return { +restrict:"E", +scope:{ +resource:"=", +name:"=" +}, +templateUrl:"views/directives/osc-image-summary.html" +}; +}), angular.module("openshiftConsole").controller("KeyValuesEntryController", [ "$scope", function(a) { +a.editing = !1, a.edit = function() { +a.originalValue = a.value, a.editing = !0; +}, a.cancel = function() { +a.value = a.originalValue, a.editing = !1; +}, a.update = function(b, c, d) { +c && (d[b] = c, a.editing = !1); +}; +} ]).controller("KeyValuesController", [ "$scope", function(a) { +var b = {}; +a.allowDelete = function(c) { +return "never" === a.deletePolicy ? !1 :"added" === a.deletePolicy ? void 0 !== b[c] :!0; +}, a.addEntry = function() { +if (a.key && a.value) { +var c = a.readonlyKeys.split(","); +if (-1 !== c.indexOf(a.key)) return; +b[a.key] = "", a.entries[a.key] = a.value, a.key = null, a.value = null, a.form.$setPristine(), a.form.$setUntouched(), a.form.$setValidity(); +} +}, a.deleteEntry = function(c) { +a.entries[c] && (delete a.entries[c], delete b[c]); +}; +} ]).directive("oscInputValidator", function() { +var a = { +always:function() { +return !0; +}, +env:function(a, b) { +var c = /^[A-Za-z_][A-Za-z0-9_]*$/i; +return void 0 === a || null === a || 0 === a.trim().length ? !0 :c.test(b); +}, +label:function(a, b) { +function c(a) { +return a.length > h ? !1 :g.test(a); +} +function d(a) { +return a.length > f ? !1 :e.test(a); +} +var e = /^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$/, f = 63, g = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/, h = 253; +if (void 0 === a || null === a || 0 === a.trim().length) return !0; +var i = b.split("/"); +switch (i.length) { +case 1: +return d(i[0]); + +case 2: +return c(i[0]) && d(i[1]); +} +return !1; +} +}; +return { +require:[ "ngModel", "^oscKeyValues" ], +restrict:"A", +link:function(b, c, d, e) { +var f = e[0], g = e[1]; +"key" === d.oscInputValidator ? f.$validators.oscKeyValid = a[g.scope.keyValidator] :"value" === d.oscInputValidator && (f.$validators.oscValueValid = a[g.scope.valueValidator]); +} +}; +}).directive("oscKeyValues", function() { +return { +restrict:"E", +scope:{ +keyTitle:"@", +entries:"=", +delimiter:"@", +editable:"@", +keyValidator:"@", +valueValidator:"@", +deletePolicy:"@", +readonlyKeys:"@", +keyValidationTooltip:"@", +valueValidationTooltip:"@" +}, +controller:[ "$scope", function(a) { +this.scope = a; +} ], +templateUrl:"views/directives/osc-key-values.html", +compile:function(a, b) { +b.delimiter || (b.delimiter = ":"), b.keyTitle || (b.keyTitle = "Name"), b.editable = b.editable && "true" !== b.editable ? !1 :!0, b.keyValidator || (b.keyValidator = "always"), b.valueValidator || (b.valueValidator = "always"), -1 === [ "always", "added", "none" ].indexOf(b.deletePolicy) && (b.deletePolicy = "always"), b.readonlyKeys || (b.readonlyKeys = ""); +} +}; +}), angular.module("openshiftConsole").directive("oscResourceNameValidator", function() { +var a = 24, b = /^[a-z]([-a-z0-9]*[a-z0-9])?/i; +return { +require:"ngModel", +link:function(c, d, e, f) { +f.$validators.oscResourceNameValidator = function(c, d) { +return f.$isEmpty(c) ? !1 :null === d ? !1 :f.$isEmpty(d.trim()) ? !1 :c.length <= a && b.test(d) && -1 === d.indexOf(" ") ? !0 :!1; +}; +} +}; +}), angular.module("openshiftConsole").directive("oscRouting", function() { +return { +require:"^form", +restrict:"E", +scope:{ +route:"=model", +uriDisabled:"=", +uriRequired:"=" +}, +templateUrl:"views/directives/osc-routing.html", +link:function(a, b, c, d) { +a.form = d; +} +}; }), angular.module("openshiftConsole").directive("podTemplate", function() { return { restrict:"E", @@ -14308,36 +14883,10 @@ template:'{{id.substring(0, 6)}}' }), angular.module("openshiftConsole").directive("labels", function() { return { restrict:"E", -templateUrl:"views/_labels.html", scope:{ labels:"=" -} -}; -}).directive("labelValidator", function() { -return { -restrict:"A", -require:"ngModel", -link:function(a, b, c, d) { -d.$validators.label = function(a, b) { -function c(a) { -return a.length > i ? !1 :h.test(a); -} -function e(a) { -return a.length > g ? !1 :f.test(a); -} -var f = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/, g = 63, h = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/, i = 253; -if (d.$isEmpty(a)) return !0; -var j = b.split("/"); -switch (j.length) { -case 1: -return e(j[0]); - -case 2: -return c(j[0]) && e(j[1]); -} -return !1; -}; -} +}, +templateUrl:"views/directives/labels.html" }; }), angular.module("openshiftConsole").directive("templateOptions", function() { return { @@ -14374,6 +14923,37 @@ a.url(c.toString()); }); } }; +} ]).directive("catalogImage", [ "$location", "Logger", function(a, b) { +return { +restrict:"E", +scope:{ +image:"=", +imageRepo:"=", +imageTag:"=", +project:"=", +sourceUrl:"=" +}, +templateUrl:"views/catalog/_image.html", +link:function(c, d) { +$(".select-image", d).click(function() { +$(".modal", d).on("hidden.bs.modal", function() { +c.$apply(function() { +b.info(c); +var d = URI.expand("/project/{project}/create/fromimage{?q*}", { +project:c.project, +q:{ +imageName:c.imageRepo.metadata.name, +imageTag:c.imageTag, +namespace:c.imageRepo.metadata.namespace, +sourceURL:c.sourceUrl +} +}); +a.url(d.toString()); +}); +}).modal("hide"); +}); +} +}; } ]), angular.module("openshiftConsole").filter("dateRelative", function() { return function(a) { return a ? moment(a).fromNow() :a; @@ -14401,6 +14981,10 @@ return moment(a.metadata.creationTimestamp).diff(moment(b.metadata.creationTimes }; }), angular.module("openshiftConsole").filter("annotation", function() { return function(a, b) { +if (a && a.spec && a.spec.tags && -1 !== b.indexOf(".")) for (var c = b.split("."), d = a.spec.tags, e = 0; e < d.length; ++e) { +var f = d[e], g = c[0], h = c[1]; +if (g === f.name && f.annotations) return f.annotations[h]; +} return a && a.metadata && a.metadata.annotations ? a.metadata.annotations[b] :null; }; }).filter("description", [ "annotationFilter", function(a) { @@ -14408,9 +14992,10 @@ return function(b) { return a(b, "description"); }; } ]).filter("tags", [ "annotationFilter", function(a) { -return function(b) { -var c = a(b, "tags"); -return c ? c.split(/\s*,\s*/) :[]; +return function(b, c) { +c = c || "tags"; +var d = a(b, c); +return d ? d.split(/\s*,\s*/) :[]; }; } ]).filter("label", function() { return function(a, b) { @@ -14422,9 +15007,10 @@ var c = a(b, "icon"); return c ? c :""; }; } ]).filter("iconClass", [ "annotationFilter", function(a) { -return function(b, c) { -var d = a(b, "iconClass"); -return d ? d :"template" === c ? "fa fa-bolt" :""; +return function(b, c, d) { +d = d || "iconClass"; +var e = a(b, d); +return e ? e :"template" === c ? "fa fa-bolt" :"image" === c ? "fa fa-cube" :""; }; } ]).filter("imageName", function() { return function(a) { @@ -14432,6 +15018,14 @@ if (!a) return ""; var b, c = a.split("/"); return 3 === c.length ? (b = c[2].split(":"), c[1] + "/" + b[0]) :2 === c.length ? a :1 === c.length ? (b = a.split(":"), b[0]) :void 0; }; +}).filter("imageEnv", function() { +return function(a, b) { +for (var c = a.dockerImageMetadata.Config.Env, d = 0; d < c.length; d++) { +var e = c[d].split("="); +if (e[0] === b) return e[1]; +} +return null; +}; }).filter("buildForImage", function() { return function(a, b) { for (var c = a.dockerImageMetadata.Config.Env, d = 0; d < c.length; d++) { @@ -14479,7 +15073,15 @@ c = "" == c ? c :c + "/"; var d = c + a.name; return d += " [" + b + "]"; }; -}), angular.module("openshiftConsole").filter("hashSize", function() { +}), angular.module("openshiftConsole").filter("underscore", function() { +return function(a) { +return a.replace(/\./g, "_"); +}; +}).filter("defaultIfBlank", function() { +return function(a, b) { +return null === a ? b :("string" != typeof a && (a = String(a)), 0 === a.trim().length ? b :a); +}; +}).filter("hashSize", function() { return function(a) { return a ? Object.keys(a).length :0; }; @@ -14575,6 +15177,28 @@ return "http://docs.openshift.org/latest/welcome/index.html"; return function(a) { return "completed" !== a.status ? a.titles.started :a.hasErrors ? a.titles.failure :a.titles.success; }; +}).filter("httpHttps", function() { +return function(a) { +return a ? "https://" :"http://"; +}; +}).filter("yesNo", function() { +return function(a) { +return a ? "Yes" :"No"; +}; +}).filter("valuesIn", function() { +return function(a, b) { +var c = b.split(","), d = {}; +return angular.forEach(a, function(a, b) { +-1 !== c.indexOf(b) && (d[b] = a); +}), d; +}; +}).filter("valuesNotIn", function() { +return function(a, b) { +var c = b.split(","), d = {}; +return angular.forEach(a, function(a, b) { +-1 === c.indexOf(b) && (d[b] = a); +}), d; +}; });`) func scripts_scripts_js() ([]byte, error) { @@ -57759,11 +58383,6 @@ select[multiple].input-lg,textarea.input-lg{height:auto} .btn-block{display:block;width:100%;padding-left:0;padding-right:0} .btn-block+.btn-block{margin-top:5px} input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%} -.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear} -.fade.in{opacity:1} -.collapse{display:none} -.collapse.in{display:block} -.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease} @font-face{font-family:'Glyphicons Halflings';src:url(../../components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot);src:url(../../components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../../components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff) format('woff'),url(../../components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../../components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')} .glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} .glyphicon-asterisk:before{content:"\2a"} @@ -59643,7 +60262,7 @@ to{transform:rotate(359deg)}} .tile .tile-table+p{margin-top:3px;font-size:inherit} .tile.tile-template a.label{font-size:11px} .tile.tile-project h2{margin:10px 0} -.tile.tile-status{background-color:#e6ecf1;border-top:5px solid #bfcedb} +.tile.tile-status{background-color:#e6ecf1;border-top:5px solid #bfcedb;margin-top:40px} .tile-click{cursor:pointer;position:relative} .tile-click:hover{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)} .tile-click:hover .btn{color:#000!important} @@ -59688,6 +60307,8 @@ ul.messenger-theme-flat .messenger-message.alert-warning:before{content:"\e60c"; ul.messenger-theme-flat .messenger-message.alert-warning .messenger-message-inner:before{color:#fff;content:"\e608";background-color:transparent} ul.messenger-theme-flat .messenger-message.alert-success .messenger-message-inner:before{color:#5cb75c;content:"\e602";background-color:transparent} ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:before{color:#27799c;content:"\e604";background-color:transparent} +.btn-file{position:relative;overflow:hidden} +.btn-file input[type=file]{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;filter:alpha(opacity=0);opacity:0;cursor:inherit;display:block} .pod{padding:10px;border-radius:10px;margin-bottom:5px;display:inline-block;background-color:rgba(204,204,204,.15);border:1px solid rgba(170,170,170,.15)} .pod.pod-running{background-color:rgba(117,198,247,.15);border:1px solid rgba(66,147,196,.15)} .pod+.pod{margin-left:5px} @@ -59812,8 +60433,8 @@ td.visible-print,th.visible-print{display:table-cell!important}} body,html{height:100%} .console-os{background-color:#f8f8f8} .console-os #content-wrap,.console-os #content-wrap .content{height:100%} -.console-os .container-main{background-color:#f8f8f8;-webkit-flex:1;-moz-flex:1;-ms-flex:1;flex:1} -.console-os #content-wrap>.container{margin-top:35px} +.console-os .container-main{background-color:#f8f8f8;padding-bottom:80px;-webkit-flex:1;-moz-flex:1;-ms-flex:1;flex:1} +.console-os #content-wrap>.container{margin-top:35px;margin-bottom:80px} .console-os #content-wrap>.container h1{margin-top:10px} .console-os .navbar{border:none;margin-bottom:0} .console-os .navbar-pf{background:#34383c} @@ -59837,43 +60458,52 @@ body,html{height:100%} @media (min-width:1600px){.console-os .navbar-project .navbar-project-menu{width:380px} .console-os .navbar-project .navbar-search{margin-left:420px}} .dl-horizontal.left dt{text-align:left;font-weight:400} -.create-from-template .template-name{text-align:right} -.create-from-template .template-name span.fa{font-size:40px} -@media (min-width:768px){.create-from-template .template-name span.fa{font-size:100px}} -.create-from-template span.fa.visible-xs-inline{margin-right:10px} -.flow{display:table;width:100%} -.flow>.flow-block{display:inline-block} -.flow>.flow-block .action,.flow>.flow-block.right{font-size:11px;font-weight:400} -.flow>.flow-block>ul.list-inline{margin-bottom:0} -.flow>.flow-block>ul.list-inline>li{font-size:11px;text-align:left} -@media (min-width:767px){.flow>.flow-block{display:table-cell} -.flow>.flow-block.right{text-align:right}} +.create-from-image .template-name,.create-from-template .template-name{text-align:right} +.create-from-image .template-name span.fa,.create-from-template .template-name span.fa{font-size:40px} +@media (min-width:768px){.create-from-image .template-name span.fa,.create-from-template .template-name span.fa{font-size:100px}} +.create-from-image span.fa.visible-xs-inline,.create-from-template span.fa.visible-xs-inline{margin-right:10px} +.create-from-image .flow,.create-from-template .flow{border-top:1px solid rgba(0,0,0,.15);display:table;margin-top:60px;width:100%} +.create-from-image .flow>.flow-block,.create-from-template .flow>.flow-block{display:inline-block} +.create-from-image .flow>.flow-block .action,.create-from-image .flow>.flow-block.right,.create-from-template .flow>.flow-block .action,.create-from-template .flow>.flow-block.right{font-size:11px;font-weight:400} +.create-from-image .flow>.flow-block>ul.list-inline,.create-from-template .flow>.flow-block>ul.list-inline{margin-bottom:0} +.create-from-image .flow>.flow-block>ul.list-inline>li,.create-from-template .flow>.flow-block>ul.list-inline>li{font-size:11px;text-align:left} +@media (min-width:767px){.create-from-image .flow>.flow-block,.create-from-template .flow>.flow-block{display:table-cell} +.create-from-image .flow>.flow-block.right,.create-from-template .flow>.flow-block.right{text-align:right}} .env-variable-list li:first-child,.label-list li:first-child{padding:6px 0 0} .env-variable-list li .key,.env-variable-list li .value,.label-list li .key,.label-list li .value{display:inline-block;margin:0;width:44%} .env-variable-list li .key,.label-list li .key{margin-left:2px} .env-variable-list li .btn,.label-list li .btn{vertical-align:top} .modal.modal-create .modal-content{padding:0} .modal.modal-create .modal-content .modal-header{background-color:transparent} -.modal.modal-create .modal-content .modal-body .template-icon{text-align:center;font-size:80px;line-height:80px} -@media (min-width:768px){.modal.modal-create .modal-content .modal-body .template-icon{font-size:130px;line-height:130px}} +.modal.modal-create .modal-content .modal-body .image-icon,.modal.modal-create .modal-content .modal-body .template-icon{text-align:center;font-size:80px;line-height:80px} +@media (min-width:768px){.modal.modal-create .modal-content .modal-body .image-icon,.modal.modal-create .modal-content .modal-body .template-icon{font-size:130px;line-height:130px}} .modal.modal-create .modal-content .modal-footer .btn-block{padding:10px} @media (min-width:768px){.modal.modal-create .modal-content{padding:25px}} .label+.label{margin-left:3px} -.action-inline{margin-left:15px} +.action-inline{margin-left:5px;font-size:11px} .action-inline i.fa,.action-inline i.pficon{color:#4d5258;margin-right:5px} .btn-group-xs>.btn,.btn-xs{padding:0 4px} .btn-group-lg>.btn,.btn-lg{line-height:1.334} code{white-space:normal} -.gutter-top-bottom{padding:15px 0} -.gutter-top{padding-top:15px} -.gutter-bottom{padding-bottom:15px} +.gutter-top-bottom{padding:20px 0} +.gutter-top-bottom.gutter-top-bottom-2x{padding:40px 0} +.gutter-top{padding-top:20px} +.gutter-top.gutter-top-2x{padding-top:40px} +.gutter-bottom{padding-bottom:20px} +.gutter-bottom.gutter-bottom-2x{padding-bottom:40px} select:invalid{box-shadow:none} .truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .well h1:first-child,.well h2:first-child,.well h3:first-child,.well h4:first-child,.well h5:first-child{margin-top:0} .attention-message{background-color:#79cef2;border:1px solid #138cbf;position:absolute;top:20%;left:50%;transform:translate(-50%,-50%);padding:1em 1em 2em;min-width:85%} .attention-message h1,.attention-message p{text-align:center} .learn-more-block{display:block;font-size:11px;font-weight:400} -.short-id{background-color:#f1f1f1;color:#666}`) +.short-id{background-color:#f1f1f1;color:#666} +.input-number{width:60px} +.fade{opacity:0;-webkit-transition:opacity .2s ease 0s;transition:opacity .2s ease 0s} +.fade.in{opacity:1} +.collapse{display:none} +.collapse.in{display:block} +.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .1s ease;transition:height .1s ease}`) func styles_main_css() ([]byte, error) { return _styles_main_css, nil @@ -60070,71 +60700,6 @@ func views_deployment_config_metadata_html() ([]byte, error) { return _views_deployment_config_metadata_html, nil } -var _views_labels_html = []byte(`
-
-
-
-

Labels

-
- -
-
-
-
- - -
-: -
- - -
- -
-Please enter a valid object label - - - - - - -
-
-
    -
  • -template -{{ labels.template }} -
  • -
  • -{{ key }} -{{ value }} - -
  • -
-
-
    -
  • -template -{{ labels.template }} -
  • -
  • -{{ key }} -{{ value }} -
  • -
-
-
`) - -func views_labels_html() ([]byte, error) { - return _views_labels_html, nil -} - var _views_pod_template_html = []byte(`
@@ -60245,13 +60810,13 @@ var _views_project_nav_html = []byte(`
- + Create - + Create @@ -60375,7 +60940,7 @@ var _views_templateopt_html = []byte(`
@@ -60657,6 +61222,60 @@ func views_builds_html() ([]byte, error) { return _views_builds_html, nil } +var _views_catalog_image_html = []byte(`
+
+
+
+ +
+
+

+{{imageRepo.metadata.name}}:{{imageTag}} +

+

{{imageRepo | annotation : (imageTag + '.description')}}

+
+
+
+ +
`) + +func views_catalog_image_html() ([]byte, error) { + return _views_catalog_image_html, nil +} + var _views_catalog_template_html = []byte(`
@@ -60711,8 +61330,43 @@ func views_catalog_template_html() ([]byte, error) { return _views_catalog_template_html, nil } -var _views_catalog_html = []byte(`
-
+var _views_catalog_images_html = []byte(`
+
+
+
+
+ +

Select a builder image

+
+
+
There are no builder images to select from. To add a builder to your project run osc create -f <image_repository_file> -n {{projectName}}
+
+
+ +
+
+
+

+All images + + + + +

+
+ +
+
+
+
+
`) + +func views_catalog_images_html() ([]byte, error) { + return _views_catalog_images_html, nil +} + +var _views_catalog_templates_html = []byte(`
+
@@ -60721,24 +61375,247 @@ var _views_catalog_html = []byte(`
There are no templates to select from. To add a template to your project run osc create -f <template_file> -n {{projectName}}
-
+
+
+ +
+
+
+
+
`) + +func views_catalog_templates_html() ([]byte, error) { + return _views_catalog_templates_html, nil +} + +var _views_create_fromimage_html = []byte(`
+
+
+
+
+
+ +
+
+ +
+
+

+Name +

+
+ +
+

Used to uniquely identify within this project all the resources created to support the application.

+
+Please enter a valid name. +

A valid name is applied to all generated resources and is an alphanumeric (a-z, and 0-9) string, with a maximum length of 24 characters, with the '-' character allowed anywhere except the first or last character.

+
+
+
+This name is already in use within the project. Please choose a different name. +
+
+ +
+
+ +{{routing | yesNo}} +
+
+
+
+ +
+
+
+ + +
+

Autodeploy when

+
+ +{{deploymentConfig.deployOnNewImage | yesNo}} +
+
+ +{{deploymentConfig.deployOnConfigChange | yesNo}} +
+

Environment Variables + + + +

+ +
+
+

Autodeploy when

+
+ +
+
+ +
+
+

Environment Variables + + + +

+ +
+
+
+ + +
+
+ +{{buildConfig.sourceUrl | defaultIfBlank: "Not Specified"}} +
+
+ +{{buildConfig.buildOnSourceChange | yesNo}} +
+
+ +{{buildConfig.buildOnImageChange | yesNo}} +
+
+
+
+ +{{buildConfig.sourceUrl}} +
+
+ +
+
+ +
+
+
+ + +
+
+ +{{scaling.replicas}} +
+
+
+ +
+Replicas must be an integer value greater than or equal to 0 +
+
+
+ + +
+
+ + After creation, these settings can only be modified through the osc command. +
+ + +Cancel +
+ +
+{{ emptyMessage }} +
+
+
+
+
+
+
`) + +func views_create_fromimage_html() ([]byte, error) { + return _views_create_fromimage_html, nil +} + +var _views_create_html = []byte(`
+
+
+
+
+

Create from ...

+ +
+

Source repository

+
+
+
+ + + + + + +
+Please enter a valid URL +
+
+ +
+

Instant apps

+
+
+There are no instant apps available to create. +
+Browse all templates... +
+
-
-

All templates

-
-
`) -func views_catalog_html() ([]byte, error) { - return _views_catalog_html, nil +func views_create_html() ([]byte, error) { + return _views_create_html, nil } var _views_deployments_html = []byte(`
@@ -60780,6 +61657,222 @@ func views_directives_copy_to_clipboard_html() ([]byte, error) { return _views_directives_copy_to_clipboard_html, nil } +var _views_directives_labels_html = []byte(` +
+ + +
+
+
+None +
+ + +
+
`) + +func views_directives_labels_html() ([]byte, error) { + return _views_directives_labels_html, nil +} + +var _views_directives_osc_file_input_html = []byte(`
+
+ + +Browse… + + + +
+ +
+There was an error reading the file. Please copy the file content into the text area. +
+
`) + +func views_directives_osc_file_input_html() ([]byte, error) { + return _views_directives_osc_file_input_html, nil +} + +var _views_directives_osc_form_section_html = []byte(`
+
+

{{header}}

+
+ +
+
`) + +func views_directives_osc_form_section_html() ([]byte, error) { + return _views_directives_osc_form_section_html, nil +} + +var _views_directives_osc_image_summary_html = []byte(`

{{ name || resource.metadata.name }}

+ +
{{ resource | description }}
`) + +func views_directives_osc_image_summary_html() ([]byte, error) { + return _views_directives_osc_image_summary_html, nil +} + +var _views_directives_osc_key_values_html = []byte(`
+
+
+
+ +
+{{delimiter}} +
+ +
+ +
+Please enter a valid key + + + + + + +
+
+Please enter a valid value + + + + + + +
+ +
    +
  • +{{key}} +{{value}} +
  • +
  • + +{{key}} + +{{ value}} + + + + + + + + +
    + + + + + + + +
    +
    +
    +
  • +
+
+
    +
  • +{{key}} +{{ value }} +
  • +
+
`) + +func views_directives_osc_key_values_html() ([]byte, error) { + return _views_directives_osc_key_values_html, nil +} + +var _views_directives_osc_routing_html = []byte(` +
+
+
+ + + +
+ +
+Please enter a valid uri for this route. +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+A certificate, private key and CA certificate are required when providing your own certs. +
+
+
+
+
`) + +func views_directives_osc_routing_html() ([]byte, error) { + return _views_directives_osc_routing_html, nil +} + var _views_images_html = []byte(`
@@ -60836,19 +61929,11 @@ var _views_newfromtemplate_html = []byte(`
-
+
-
-

{{ template.metadata.name }}

- -
{{ template | description }}
-
-

Source

{{ templateUrl }} -
+
+

Images

    @@ -60864,8 +61949,8 @@ var _views_newfromtemplate_html = []byte(`
    Items will be created in the {{ projectDisplayName() }} project.
    - -Cancel + +Cancel
@@ -61398,7 +62483,6 @@ var _bindata = map[string]func() ([]byte, error){ "styles/vendor.css": styles_vendor_css, "views/_alerts.html": views_alerts_html, "views/_deployment-config-metadata.html": views_deployment_config_metadata_html, - "views/_labels.html": views_labels_html, "views/_pod-template.html": views_pod_template_html, "views/_pods.html": views_pods_html, "views/_project-nav.html": views_project_nav_html, @@ -61409,11 +62493,21 @@ var _bindata = map[string]func() ([]byte, error){ "views/_templateopt.html": views_templateopt_html, "views/_triggers.html": views_triggers_html, "views/builds.html": views_builds_html, + "views/catalog/_image.html": views_catalog_image_html, "views/catalog/_template.html": views_catalog_template_html, - "views/catalog.html": views_catalog_html, + "views/catalog/images.html": views_catalog_images_html, + "views/catalog/templates.html": views_catalog_templates_html, + "views/create/fromimage.html": views_create_fromimage_html, + "views/create.html": views_create_html, "views/deployments.html": views_deployments_html, "views/directives/_click-to-reveal.html": views_directives_click_to_reveal_html, "views/directives/_copy-to-clipboard.html": views_directives_copy_to_clipboard_html, + "views/directives/labels.html": views_directives_labels_html, + "views/directives/osc-file-input.html": views_directives_osc_file_input_html, + "views/directives/osc-form-section.html": views_directives_osc_form_section_html, + "views/directives/osc-image-summary.html": views_directives_osc_image_summary_html, + "views/directives/osc-key-values.html": views_directives_osc_key_values_html, + "views/directives/osc-routing.html": views_directives_osc_routing_html, "views/images.html": views_images_html, "views/newfromtemplate.html": views_newfromtemplate_html, "views/pods.html": views_pods_html2, @@ -61640,8 +62734,6 @@ var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ }}, "_deployment-config-metadata.html": &_bintree_t{views_deployment_config_metadata_html, map[string]*_bintree_t{ }}, - "_labels.html": &_bintree_t{views_labels_html, map[string]*_bintree_t{ - }}, "_pod-template.html": &_bintree_t{views_pod_template_html, map[string]*_bintree_t{ }}, "_pods.html": &_bintree_t{views_pods_html, map[string]*_bintree_t{ @@ -61663,10 +62755,20 @@ var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ "builds.html": &_bintree_t{views_builds_html, map[string]*_bintree_t{ }}, "catalog": &_bintree_t{nil, map[string]*_bintree_t{ + "_image.html": &_bintree_t{views_catalog_image_html, map[string]*_bintree_t{ + }}, "_template.html": &_bintree_t{views_catalog_template_html, map[string]*_bintree_t{ }}, + "images.html": &_bintree_t{views_catalog_images_html, map[string]*_bintree_t{ + }}, + "templates.html": &_bintree_t{views_catalog_templates_html, map[string]*_bintree_t{ + }}, }}, - "catalog.html": &_bintree_t{views_catalog_html, map[string]*_bintree_t{ + "create": &_bintree_t{nil, map[string]*_bintree_t{ + "fromimage.html": &_bintree_t{views_create_fromimage_html, map[string]*_bintree_t{ + }}, + }}, + "create.html": &_bintree_t{views_create_html, map[string]*_bintree_t{ }}, "deployments.html": &_bintree_t{views_deployments_html, map[string]*_bintree_t{ }}, @@ -61675,6 +62777,18 @@ var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ }}, "_copy-to-clipboard.html": &_bintree_t{views_directives_copy_to_clipboard_html, map[string]*_bintree_t{ }}, + "labels.html": &_bintree_t{views_directives_labels_html, map[string]*_bintree_t{ + }}, + "osc-file-input.html": &_bintree_t{views_directives_osc_file_input_html, map[string]*_bintree_t{ + }}, + "osc-form-section.html": &_bintree_t{views_directives_osc_form_section_html, map[string]*_bintree_t{ + }}, + "osc-image-summary.html": &_bintree_t{views_directives_osc_image_summary_html, map[string]*_bintree_t{ + }}, + "osc-key-values.html": &_bintree_t{views_directives_osc_key_values_html, map[string]*_bintree_t{ + }}, + "osc-routing.html": &_bintree_t{views_directives_osc_routing_html, map[string]*_bintree_t{ + }}, }}, "images.html": &_bintree_t{views_images_html, map[string]*_bintree_t{ }},