diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js
index 4f9d83094..82ca08184 100644
--- a/app/components/dashboard/dashboardController.js
+++ b/app/components/dashboard/dashboardController.js
@@ -68,6 +68,7 @@ function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, System
$('#loadingViewSpinner').show();
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
+ var endpointRole = $scope.applicationState.endpoint.mode.role;
$q.all([
Container.query({all: 1}).$promise,
@@ -75,8 +76,8 @@ function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, System
Volume.query({}).$promise,
Network.query({}).$promise,
SystemService.info(),
- endpointProvider === 'DOCKER_SWARM_MODE' ? ServiceService.services() : [],
- endpointProvider === 'DOCKER_SWARM_MODE' ? StackService.stacks(true) : []
+ endpointProvider === 'DOCKER_SWARM_MODE' && endpointRole === 'MANAGER' ? ServiceService.services() : [],
+ endpointProvider === 'DOCKER_SWARM_MODE' && endpointRole === 'MANAGER' ? StackService.stacks(true) : []
]).then(function (d) {
prepareContainerData(d[0]);
prepareImageData(d[1]);
diff --git a/app/components/endpoints/endpointsController.js b/app/components/endpoints/endpointsController.js
index 9a761a164..01e9670e5 100644
--- a/app/components/endpoints/endpointsController.js
+++ b/app/components/endpoints/endpointsController.js
@@ -1,6 +1,6 @@
angular.module('endpoints', [])
-.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination',
-function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) {
+.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination',
+function ($scope, $state, $filter, EndpointService, EndpointProvider, Notifications, Pagination) {
$scope.state = {
uploadInProgress: false,
selectedItemCount: 0,
@@ -44,7 +44,7 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
$scope.addEndpoint = function() {
var name = $scope.formValues.Name;
- var URL = $scope.formValues.URL;
+ var URL = $filter('stripprotocol')($scope.formValues.URL);
var PublicURL = $scope.formValues.PublicURL;
if (PublicURL === '') {
PublicURL = URL.split(':')[0];
diff --git a/app/components/images/images.html b/app/components/images/images.html
index 8e82ff1a6..818f55b72 100644
--- a/app/components/images/images.html
+++ b/app/components/images/images.html
@@ -125,10 +125,7 @@
{{ image.Id|truncate:20}}
-
- Unused
-
+ Unused
{{ tag }}
diff --git a/app/components/network/networkController.js b/app/components/network/networkController.js
index f4ea325e5..87da9d9e8 100644
--- a/app/components/network/networkController.js
+++ b/app/components/network/networkController.js
@@ -40,12 +40,14 @@ function ($scope, $state, $transition$, $filter, Network, NetworkService, Contai
var containersInNetwork = [];
containers.forEach(function(container) {
var containerInNetwork = network.Containers[container.Id];
- containerInNetwork.Id = container.Id;
- // Name is not available in Docker 1.9
- if (!containerInNetwork.Name) {
- containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]);
+ if (containerInNetwork) {
+ containerInNetwork.Id = container.Id;
+ // Name is not available in Docker 1.9
+ if (!containerInNetwork.Name) {
+ containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]);
+ }
+ containersInNetwork.push(containerInNetwork);
}
- containersInNetwork.push(containerInNetwork);
});
$scope.containersInNetwork = containersInNetwork;
}
@@ -68,7 +70,7 @@ function ($scope, $state, $transition$, $filter, Network, NetworkService, Contai
});
} else {
Container.query({
- filters: {network: [$transition$.params().id]}
+ filters: { network: [$transition$.params().id] }
}, function success(data) {
filterContainersInNetwork(network, data);
$('#loadingViewSpinner').hide();
diff --git a/app/components/secrets/secrets.html b/app/components/secrets/secrets.html
index b274ae777..efd4107b3 100644
--- a/app/components/secrets/secrets.html
+++ b/app/components/secrets/secrets.html
@@ -16,7 +16,7 @@
@@ -39,8 +39,8 @@
Created at
-
-
+
+
diff --git a/app/components/secrets/secretsController.js b/app/components/secrets/secretsController.js
index 1aa890504..fc71dd58b 100644
--- a/app/components/secrets/secretsController.js
+++ b/app/components/secrets/secretsController.js
@@ -1,6 +1,6 @@
angular.module('secrets', [])
-.controller('SecretsController', ['$scope', '$transition$', '$state', 'SecretService', 'Notifications', 'Pagination',
-function ($scope, $transition$, $state, SecretService, Notifications, Pagination) {
+.controller('SecretsController', ['$scope', '$state', 'SecretService', 'Notifications', 'Pagination',
+function ($scope, $state, SecretService, Notifications, Pagination) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('secrets');
diff --git a/app/components/service/includes/configs.html b/app/components/service/includes/configs.html
new file mode 100644
index 000000000..e1eba62cd
--- /dev/null
+++ b/app/components/service/includes/configs.html
@@ -0,0 +1,62 @@
+
diff --git a/app/components/service/service.html b/app/components/service/service.html
index e11585c62..f54a46b69 100644
--- a/app/components/service/service.html
+++ b/app/components/service/service.html
@@ -117,6 +117,7 @@
Restart policy
Update configuration
Service labels
+ Configs
Secrets
Tasks
@@ -164,6 +165,7 @@
+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js
index 9ff2cb6e2..bf3f6d514 100644
--- a/app/components/service/serviceController.js
+++ b/app/components/service/serviceController.js
@@ -1,6 +1,6 @@
angular.module('service', [])
-.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'SecretService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService',
-function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, SecretService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) {
+.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService',
+function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
@@ -59,6 +59,21 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}
};
+ $scope.addConfig = function addConfig(service, config) {
+ if (config && service.ServiceConfigs.filter(function(serviceConfig) { return serviceConfig.Id === config.Id;}).length === 0) {
+ service.ServiceConfigs.push({ Id: config.Id, Name: config.Name, FileName: config.Name, Uid: '0', Gid: '0', Mode: 292 });
+ updateServiceArray(service, 'ServiceConfigs', service.ServiceConfigs);
+ }
+ };
+ $scope.removeConfig = function removeSecret(service, index) {
+ var removedElement = service.ServiceConfigs.splice(index, 1);
+ if (removedElement !== null) {
+ updateServiceArray(service, 'ServiceConfigs', service.ServiceConfigs);
+ }
+ };
+ $scope.updateConfig = function updateConfig(service) {
+ updateServiceArray(service, 'ServiceConfigs', service.ServiceConfigs);
+ };
$scope.addSecret = function addSecret(service, secret) {
if (secret && service.ServiceSecrets.filter(function(serviceSecret) { return serviceSecret.Id === secret.Id;}).length === 0) {
service.ServiceSecrets.push({ Id: secret.Id, Name: secret.Name, FileName: secret.Name, Uid: '0', Gid: '0', Mode: 444 });
@@ -193,6 +208,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceContainerLabels);
config.TaskTemplate.ContainerSpec.Image = service.Image;
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
+ config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : [];
if (service.Mode === 'replicated') {
config.Mode.Replicated.Replicas = service.Replicas;
@@ -289,6 +305,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
function translateServiceArrays(service) {
service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
+ service.ServiceConfigs = service.Configs ? service.Configs.map(ConfigHelper.flattenConfig) : [];
service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env);
service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels);
service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels);
@@ -323,12 +340,14 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
return $q.all({
tasks: TaskService.tasks({ service: [service.Name] }),
nodes: NodeService.nodes(),
- secrets: apiVersion >= 1.25 ? SecretService.secrets() : []
+ secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
+ configs: apiVersion >= 1.30 ? ConfigService.configs() : []
});
})
.then(function success(data) {
$scope.tasks = data.tasks;
$scope.nodes = data.nodes;
+ $scope.configs = data.configs;
$scope.secrets = data.secrets;
// Set max cpu value
@@ -350,6 +369,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
})
.catch(function error(err) {
$scope.secrets = [];
+ $scope.configs = [];
Notifications.error('Failure', err, 'Unable to retrieve service details');
})
.finally(function final() {
diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js
index 339adb3d1..ca5fbbe6b 100644
--- a/app/components/services/servicesController.js
+++ b/app/components/services/servicesController.js
@@ -97,19 +97,22 @@ function ($q, $scope, $transition$, $state, Service, ServiceService, ServiceHelp
$('#loadServicesSpinner').show();
$q.all({
services: Service.query({}).$promise,
- tasks: Task.query({filters: {'desired-state': ['running']}}).$promise,
+ tasks: Task.query({filters: {'desired-state': ['running','accepted']}}).$promise,
nodes: Node.query({}).$promise
})
.then(function success(data) {
$scope.swarmManagerIP = NodeHelper.getManagerIP(data.nodes);
$scope.services = data.services.map(function (service) {
- var serviceTasks = data.tasks.filter(function (task) {
+ var runningTasks = data.tasks.filter(function (task) {
return task.ServiceID === service.ID && task.Status.State === 'running';
});
+ var allTasks = data.tasks.filter(function (task) {
+ return task.ServiceID === service.ID;
+ });
var taskNodes = data.nodes.filter(function (node) {
return node.Spec.Availability === 'active' && node.Status.State === 'ready';
});
- return new ServiceViewModel(service, serviceTasks, taskNodes);
+ return new ServiceViewModel(service, runningTasks, allTasks, taskNodes);
});
})
.catch(function error(err) {
diff --git a/app/components/settings/settingsController.js b/app/components/settings/settingsController.js
index cd038dd35..3c122a053 100644
--- a/app/components/settings/settingsController.js
+++ b/app/components/settings/settingsController.js
@@ -40,6 +40,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
if (!$scope.formValues.customTemplates) {
settings.TemplatesURL = DEFAULT_TEMPLATES_URL;
}
+
settings.DisplayExternalContributors = !$scope.formValues.externalContributions;
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
diff --git a/app/components/settingsAuthentication/settingsAuthentication.html b/app/components/settingsAuthentication/settingsAuthentication.html
index 92fd050d7..38506d751 100644
--- a/app/components/settingsAuthentication/settingsAuthentication.html
+++ b/app/components/settingsAuthentication/settingsAuthentication.html
@@ -66,7 +66,7 @@
+
+ Environment
+
+
+
+
Actions
diff --git a/app/components/stack/stackController.js b/app/components/stack/stackController.js
index b483c1cc7..b8147f899 100644
--- a/app/components/stack/stackController.js
+++ b/app/components/stack/stackController.js
@@ -1,6 +1,6 @@
angular.module('stack', [])
-.controller('StackController', ['$q', '$scope', '$state', '$stateParams', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications',
-function ($q, $scope, $state, $stateParams, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications) {
+.controller('StackController', ['$q', '$scope', '$state', '$stateParams', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications', 'FormHelper',
+function ($q, $scope, $state, $stateParams, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications, FormHelper) {
$scope.deployStack = function () {
$('#createResourceSpinner').show();
@@ -8,8 +8,9 @@ function ($q, $scope, $state, $stateParams, $document, StackService, NodeService
// The codemirror editor does not work with ng-model so we need to retrieve
// the value directly from the editor.
var stackFile = $scope.editor.getValue();
+ var env = FormHelper.removeInvalidEnvVars($scope.stack.Env);
- StackService.updateStack($scope.stack.Id, stackFile)
+ StackService.updateStack($scope.stack.Id, stackFile, env)
.then(function success(data) {
Notifications.success('Stack successfully deployed');
$state.reload();
@@ -22,6 +23,14 @@ function ($q, $scope, $state, $stateParams, $document, StackService, NodeService
});
};
+ $scope.addEnvironmentVariable = function() {
+ $scope.stack.Env.push({ name: '', value: ''});
+ };
+
+ $scope.removeEnvironmentVariable = function(index) {
+ $scope.stack.Env.splice(index, 1);
+ };
+
function initView() {
$('#loadingViewSpinner').show();
var stackId = $stateParams.id;
@@ -48,7 +57,7 @@ function ($q, $scope, $state, $stateParams, $document, StackService, NodeService
$document.ready(function() {
var webEditorElement = $document[0].getElementById('web-editor');
if (webEditorElement) {
- $scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement);
+ $scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement, true, false);
}
});
diff --git a/app/components/swarm/swarm.html b/app/components/swarm/swarm.html
index bb243ef32..d9b8372c7 100644
--- a/app/components/swarm/swarm.html
+++ b/app/components/swarm/swarm.html
@@ -224,8 +224,8 @@
- {{ node.Hostname }}
- {{ node.Hostname }}
+ {{ node.Hostname }}
+ {{ node.Hostname }}
{{ node.Role }}
{{ node.CPUs / 1000000000 }}
diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html
index 8b7172b17..94ffe752c 100644
--- a/app/components/templates/templates.html
+++ b/app/components/templates/templates.html
@@ -3,14 +3,79 @@
-
+
Templates
-
+
+
+
+
+
+
+ Hide
+
+
+
-
+
+
+
+
+
+
@@ -218,7 +303,11 @@
+
+
+
+
@@ -249,53 +338,92 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ tpl.Title }}
-
-
-
-
-
-
-
-
-
-
-
- {{ tpl.Description }}
-
-
- {{ tpl.Categories.join(', ') }}
-
-
-
-
-
+
diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js
index dac5df747..8ca34a100 100644
--- a/app/components/templates/templatesController.js
+++ b/app/components/templates/templatesController.js
@@ -1,14 +1,16 @@
angular.module('templates', [])
-.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService',
-function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator, SettingsService) {
+.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', 'StackService',
+function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator, SettingsService, StackService) {
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false,
hideDescriptions: $transition$.params().hide_descriptions,
formValidationError: '',
+ showDeploymentSelector: false,
filters: {
Categories: '!',
- Platform: '!'
+ Platform: '!',
+ Type: 'container'
}
};
@@ -34,6 +36,14 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
$scope.state.selectedTemplate.Ports.splice(index, 1);
};
+ $scope.addExtraHost = function() {
+ $scope.state.selectedTemplate.Hosts.push('');
+ };
+
+ $scope.removeExtraHost = function(index) {
+ $scope.state.selectedTemplate.Hosts.splice(index, 1);
+ };
+
function validateForm(accessControlData, isAdmin) {
$scope.state.formValidationError = '';
var error = '';
@@ -46,19 +56,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
return true;
}
- $scope.createTemplate = function() {
- $('#createContainerSpinner').show();
-
- var userDetails = Authentication.getUserDetails();
- var accessControlData = $scope.formValues.AccessControlData;
- var isAdmin = userDetails.role === 1 ? true : false;
-
- if (!validateForm(accessControlData, isAdmin)) {
- $('#createContainerSpinner').hide();
- return;
- }
-
- var template = $scope.state.selectedTemplate;
+ function createContainerFromTemplate(template, userId, accessControlData) {
var templateConfiguration = createTemplateConfiguration(template);
var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
var generatedVolumeIds = [];
@@ -77,7 +75,6 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
})
.then(function success(data) {
var containerIdentifier = data.Id;
- var userId = userDetails.ID;
return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, generatedVolumeIds);
})
.then(function success() {
@@ -88,8 +85,59 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
Notifications.error('Failure', err, err.msg);
})
.finally(function final() {
- $('#createContainerSpinner').hide();
+ $('#createResourceSpinner').hide();
});
+ }
+
+ function createStackFromTemplate(template, userId, accessControlData) {
+ var stackName = $scope.formValues.name;
+
+ for (var i = 0; i < template.Env.length; i++) {
+ var envvar = template.Env[i];
+ if (envvar.set) {
+ envvar.value = envvar.set;
+ }
+ }
+
+ StackService.createStackFromGitRepository(stackName, template.Repository.url, template.Repository.stackfile, template.Env)
+ .then(function success() {
+ Notifications.success('Stack successfully created');
+ })
+ .catch(function error(err) {
+ Notifications.warning('Deployment error', err.err.data.err);
+ })
+ .then(function success(data) {
+ return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []);
+ })
+ .then(function success() {
+ $state.go('stacks', {}, {reload: true});
+ })
+ .finally(function final() {
+ $('#createResourceSpinner').hide();
+ });
+ }
+
+ $scope.createTemplate = function() {
+ $('#createResourceSpinner').show();
+
+ var userDetails = Authentication.getUserDetails();
+ var userId = userDetails.ID;
+ var accessControlData = $scope.formValues.AccessControlData;
+ var isAdmin = userDetails.role === 1 ? true : false;
+
+ if (!validateForm(accessControlData, isAdmin)) {
+ $('#createResourceSpinner').hide();
+ return;
+ }
+
+ var template = $scope.state.selectedTemplate;
+ var templatesKey = $scope.templatesKey;
+
+ if (template.Type === 'stack') {
+ createStackFromTemplate(template, userId, accessControlData);
+ } else {
+ createContainerFromTemplate(template, userId, accessControlData);
+ }
};
$scope.unselectTemplate = function() {
@@ -144,11 +192,22 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
return containerMapping;
}
- function initTemplates() {
- var templatesKey = $transition$.params().key;
- var provider = $scope.applicationState.endpoint.mode.provider;
- var apiVersion = $scope.applicationState.endpoint.apiVersion;
+ $scope.updateCategories = function(templates, type) {
+ $scope.state.filters.Categories = '!';
+ updateCategories(templates, type);
+ };
+ function updateCategories(templates, type) {
+ var availableCategories = [];
+ angular.forEach(templates, function(template) {
+ if (template.Type === type) {
+ availableCategories = availableCategories.concat(template.Categories);
+ }
+ });
+ $scope.availableCategories = _.sortBy(_.uniq(availableCategories));
+ }
+
+ function initTemplates(templatesKey, type, provider, apiVersion) {
$q.all({
templates: TemplateService.getTemplates(templatesKey),
containers: ContainerService.containers(0),
@@ -161,12 +220,9 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
settings: SettingsService.publicSettings()
})
.then(function success(data) {
- $scope.templates = data.templates;
- var availableCategories = [];
- angular.forEach($scope.templates, function(template) {
- availableCategories = availableCategories.concat(template.Categories);
- });
- $scope.availableCategories = _.sortBy(_.uniq(availableCategories));
+ var templates = data.templates;
+ updateCategories(templates, type);
+ $scope.templates = templates;
$scope.runningContainers = data.containers;
$scope.availableVolumes = data.volumes.Volumes;
var networks = data.networks;
@@ -174,17 +230,33 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
$scope.globalNetworkCount = networks.length;
var settings = data.settings;
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
- var userDetails = Authentication.getUserDetails();
- $scope.isAdmin = userDetails.role === 1 ? true : false;
})
.catch(function error(err) {
$scope.templates = [];
Notifications.error('Failure', err, 'An error occured during apps initialization.');
})
.finally(function final(){
- $('#loadTemplatesSpinner').hide();
+ $('#loadingViewSpinner').hide();
});
}
- initTemplates();
+ function initView() {
+ var templatesKey = $transition$.params().key;
+ $scope.templatesKey = templatesKey;
+
+ var userDetails = Authentication.getUserDetails();
+ $scope.isAdmin = userDetails.role === 1 ? true : false;
+
+ var endpointMode = $scope.applicationState.endpoint.mode;
+ var apiVersion = $scope.applicationState.endpoint.apiVersion;
+
+ if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' && apiVersion >= 1.25) {
+ $scope.state.filters.Type = 'stack';
+ $scope.state.showDeploymentSelector = true;
+ }
+
+ initTemplates(templatesKey, $scope.state.filters.Type, endpointMode.provider, apiVersion);
+ }
+
+ initView();
}]);
diff --git a/app/components/volume/volume.html b/app/components/volume/volume.html
index 15ebfc41e..2b9d40af4 100644
--- a/app/components/volume/volume.html
+++ b/app/components/volume/volume.html
@@ -73,3 +73,26 @@
+
+
+
+
+
+
+
+ Container Name
+ Mounted At
+ Read-only
+
+
+
+ {{ container | containername }}
+ {{ container.volumeData.Destination }}
+ {{ !container.volumeData.RW }}
+
+
+
+
+
+
+
diff --git a/app/components/volume/volumeController.js b/app/components/volume/volumeController.js
index 16c165980..12b7053a5 100644
--- a/app/components/volume/volumeController.js
+++ b/app/components/volume/volumeController.js
@@ -1,6 +1,6 @@
angular.module('volume', [])
-.controller('VolumeController', ['$scope', '$state', '$transition$', 'VolumeService', 'Notifications',
-function ($scope, $state, $transition$, VolumeService, Notifications) {
+.controller('VolumeController', ['$scope', '$state', '$transition$', 'VolumeService', 'ContainerService', 'Notifications',
+function ($scope, $state, $transition$, VolumeService, ContainerService, Notifications) {
$scope.removeVolume = function removeVolume() {
$('#loadingViewSpinner').show();
@@ -17,12 +17,27 @@ function ($scope, $state, $transition$, VolumeService, Notifications) {
});
};
+ function getVolumeDataFromContainer(container, volumeId) {
+ return container.Mounts.find(function(volume) {
+ return volume.Name === volumeId;
+ });
+ }
+
function initView() {
$('#loadingViewSpinner').show();
VolumeService.volume($transition$.params().id)
.then(function success(data) {
var volume = data;
$scope.volume = volume;
+ var containerFilter = { volume: [volume.Id] };
+ return ContainerService.containers(1, containerFilter);
+ })
+ .then(function success(data) {
+ var containers = data.map(function(container) {
+ container.volumeData = getVolumeDataFromContainer(container, $scope.volume.Id);
+ return container;
+ });
+ $scope.containersUsingVolume = containers;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve volume details');
diff --git a/app/helpers/configHelper.js b/app/helpers/configHelper.js
new file mode 100644
index 000000000..f47eacc7e
--- /dev/null
+++ b/app/helpers/configHelper.js
@@ -0,0 +1,34 @@
+angular.module('portainer.helpers')
+.factory('ConfigHelper', [function ConfigHelperFactory() {
+ 'use strict';
+ return {
+ flattenConfig: function(config) {
+ if (config) {
+ return {
+ Id: config.ConfigID,
+ Name: config.ConfigName,
+ FileName: config.File.Name,
+ Uid: config.File.UID,
+ Gid: config.File.GID,
+ Mode: config.File.Mode
+ };
+ }
+ return {};
+ },
+ configConfig: function(config) {
+ if (config) {
+ return {
+ ConfigID: config.Id,
+ ConfigName: config.Name,
+ File: {
+ Name: config.FileName || config.Name,
+ UID: config.Uid || '0',
+ GID: config.Gid || '0',
+ Mode: config.Mode || 292
+ }
+ };
+ }
+ return {};
+ }
+ };
+}]);
diff --git a/app/helpers/formHelper.js b/app/helpers/formHelper.js
new file mode 100644
index 000000000..7cb3456f1
--- /dev/null
+++ b/app/helpers/formHelper.js
@@ -0,0 +1,18 @@
+angular.module('portainer.helpers')
+.factory('FormHelper', [function FormHelperFactory() {
+ 'use strict';
+ var helper = {};
+
+ helper.removeInvalidEnvVars = function(env) {
+ for (var i = env.length - 1; i >= 0; i--) {
+ var envvar = env[i];
+ if (!envvar.value || !envvar.name) {
+ env.splice(i, 1);
+ }
+ }
+
+ return env;
+ };
+
+ return helper;
+}]);
diff --git a/app/helpers/secretHelper.js b/app/helpers/secretHelper.js
index 9c0f3d65b..afd3b1a56 100644
--- a/app/helpers/secretHelper.js
+++ b/app/helpers/secretHelper.js
@@ -22,9 +22,9 @@ angular.module('portainer.helpers')
SecretName: secret.Name,
File: {
Name: secret.FileName,
- UID: '0',
- GID: '0',
- Mode: 444
+ UID: secret.Uid || '0',
+ GID: secret.Gid || '0',
+ Mode: secret.Mode || 444
}
};
}
diff --git a/app/helpers/templateHelper.js b/app/helpers/templateHelper.js
index a4f626fd5..d6ad6bc63 100644
--- a/app/helpers/templateHelper.js
+++ b/app/helpers/templateHelper.js
@@ -15,7 +15,8 @@ angular.module('portainer.helpers')
},
PortBindings: {},
Binds: [],
- Privileged: false
+ Privileged: false,
+ ExtraHosts: []
},
Volumes: {}
};
diff --git a/app/models/api/stack.js b/app/models/api/stack.js
index 3d4645913..30b130943 100644
--- a/app/models/api/stack.js
+++ b/app/models/api/stack.js
@@ -2,6 +2,7 @@ function StackViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Checked = false;
+ this.Env = data.Env;
if (data.ResourceControl && data.ResourceControl.Id !== 0) {
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
}
diff --git a/app/models/api/stackTemplate.js b/app/models/api/stackTemplate.js
new file mode 100644
index 000000000..728d98421
--- /dev/null
+++ b/app/models/api/stackTemplate.js
@@ -0,0 +1,11 @@
+function StackTemplateViewModel(data) {
+ this.Type = data.type;
+ this.Title = data.title;
+ this.Description = data.description;
+ this.Note = data.note;
+ this.Categories = data.categories ? data.categories : [];
+ this.Platform = data.platform ? data.platform : 'undefined';
+ this.Logo = data.logo;
+ this.Repository = data.repository;
+ this.Env = data.env ? data.env : [];
+}
diff --git a/app/models/api/template.js b/app/models/api/template.js
index 0123f92e4..76a77d961 100644
--- a/app/models/api/template.js
+++ b/app/models/api/template.js
@@ -1,4 +1,5 @@
function TemplateViewModel(data) {
+ this.Type = data.type;
this.Title = data.title;
this.Description = data.description;
this.Note = data.note;
@@ -42,4 +43,5 @@ function TemplateViewModel(data) {
};
});
}
+ this.Hosts = data.hosts ? data.hosts : [];
}
diff --git a/app/models/docker/config.js b/app/models/docker/config.js
new file mode 100644
index 000000000..214909b5a
--- /dev/null
+++ b/app/models/docker/config.js
@@ -0,0 +1,15 @@
+function ConfigViewModel(data) {
+ this.Id = data.ID;
+ this.CreatedAt = data.CreatedAt;
+ this.UpdatedAt = data.UpdatedAt;
+ this.Version = data.Version.Index;
+ this.Name = data.Spec.Name;
+ this.Labels = data.Spec.Labels;
+ this.Data = atob(data.Spec.Data);
+
+ if (data.Portainer) {
+ if (data.Portainer.ResourceControl) {
+ this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
+ }
+ }
+}
diff --git a/app/models/docker/container.js b/app/models/docker/container.js
index 8c2d112f5..c37f2fe93 100644
--- a/app/models/docker/container.js
+++ b/app/models/docker/container.js
@@ -7,6 +7,7 @@ function ContainerViewModel(data) {
if (data.NetworkSettings && !_.isEmpty(data.NetworkSettings.Networks)) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
}
+ this.NetworkSettings = data.NetworkSettings;
this.Image = data.Image;
this.ImageID = data.ImageID;
this.Command = data.Command;
diff --git a/app/models/docker/containerDetails.js b/app/models/docker/containerDetails.js
index 63945ff41..eae58c105 100644
--- a/app/models/docker/containerDetails.js
+++ b/app/models/docker/containerDetails.js
@@ -9,6 +9,7 @@ function ContainerDetailsViewModel(data) {
this.Image = data.Image;
this.Config = data.Config;
this.HostConfig = data.HostConfig;
+ this.Mounts = data.Mounts;
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
diff --git a/app/models/docker/service.js b/app/models/docker/service.js
index 28d8609ba..c83779243 100644
--- a/app/models/docker/service.js
+++ b/app/models/docker/service.js
@@ -1,4 +1,4 @@
-function ServiceViewModel(data, runningTasks, nodes) {
+function ServiceViewModel(data, runningTasks, allTasks, nodes) {
this.Model = data;
this.Id = data.ID;
this.Tasks = [];
@@ -12,8 +12,8 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.Replicas = data.Spec.Mode.Replicated.Replicas;
} else {
this.Mode = 'global';
- if (nodes) {
- this.Replicas = nodes.length;
+ if (allTasks) {
+ this.Replicas = allTasks.length;
}
}
if (runningTasks) {
@@ -69,6 +69,7 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.Hosts = containerSpec.Hosts;
this.DNSConfig = containerSpec.DNSConfig;
this.Secrets = containerSpec.Secrets;
+ this.Configs = containerSpec.Configs;
}
if (data.Endpoint) {
this.Ports = data.Endpoint.Ports;
diff --git a/app/rest/docker/config.js b/app/rest/docker/config.js
new file mode 100644
index 000000000..330692fbe
--- /dev/null
+++ b/app/rest/docker/config.js
@@ -0,0 +1,12 @@
+angular.module('portainer.rest')
+.factory('Config', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ConfigFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
+ 'use strict';
+ return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/configs/:id/:action', {
+ endpointId: EndpointProvider.endpointID
+ }, {
+ get: { method: 'GET', params: { id: '@id' } },
+ query: { method: 'GET', isArray: true },
+ create: { method: 'POST', params: { action: 'create' } },
+ remove: { method: 'DELETE', params: { id: '@id' } }
+ });
+}]);
diff --git a/app/rest/docker/container.js b/app/rest/docker/container.js
index d8786cb03..25ab3b2da 100644
--- a/app/rest/docker/container.js
+++ b/app/rest/docker/container.js
@@ -40,6 +40,9 @@ angular.module('portainer.rest')
exec: {
method: 'POST', params: {id: '@id', action: 'exec'},
transformResponse: genericHandler
+ },
+ inspect: {
+ method: 'GET', params: { id: '@id', action: 'json' }
}
});
}]);
diff --git a/app/routes.js b/app/routes.js
index 0232c3d6d..265096184 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -26,6 +26,32 @@ function configureRoutes($stateProvider) {
requiresLogin: false
}
})
+ .state('configs', {
+ url: '^/configs/',
+ views: {
+ 'content@': {
+ templateUrl: 'app/components/configs/configs.html',
+ controller: 'ConfigsController'
+ },
+ 'sidebar@': {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('config', {
+ url: '^/config/:id/',
+ views: {
+ 'content@': {
+ templateUrl: 'app/components/config/config.html',
+ controller: 'ConfigController'
+ },
+ 'sidebar@': {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
.state('containers', {
parent: 'root',
url: '/containers/',
@@ -105,6 +131,19 @@ function configureRoutes($stateProvider) {
}
}
})
+ .state('inspect', {
+ url: '^/containers/:id/inspect',
+ views: {
+ 'content@': {
+ templateUrl: 'app/components/containerInspect/containerInspect.html',
+ controller: 'ContainerInspectController'
+ },
+ 'sidebar@': {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
.state('dashboard', {
parent: 'root',
url: '/dashboard',
@@ -143,6 +182,19 @@ function configureRoutes($stateProvider) {
}
}
})
+ .state('actions.create.config', {
+ url: '/config',
+ views: {
+ 'content@': {
+ templateUrl: 'app/components/createConfig/createconfig.html',
+ controller: 'CreateConfigController'
+ },
+ 'sidebar@': {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
.state('actions.create.container', {
url: '/container/:from',
views: {
diff --git a/app/services/api/stackService.js b/app/services/api/stackService.js
index 6ab6e7f8a..e206b620d 100644
--- a/app/services/api/stackService.js
+++ b/app/services/api/stackService.js
@@ -100,13 +100,19 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.createStackFromFileContent = function(name, stackFileContent) {
+ service.createStackFromFileContent = function(name, stackFileContent, env) {
var deferred = $q.defer();
SwarmService.swarm()
.then(function success(data) {
var swarm = data;
- return Stack.create({ method: 'string' }, { Name: name, SwarmID: swarm.Id, StackFileContent: stackFileContent }).$promise;
+ var payload = {
+ Name: name,
+ SwarmID: swarm.Id,
+ StackFileContent: stackFileContent,
+ Env: env
+ };
+ return Stack.create({ method: 'string' }, payload).$promise;
})
.then(function success(data) {
deferred.resolve(data);
@@ -118,13 +124,20 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.createStackFromGitRepository = function(name, gitRepository, pathInRepository) {
+ service.createStackFromGitRepository = function(name, gitRepository, pathInRepository, env) {
var deferred = $q.defer();
SwarmService.swarm()
.then(function success(data) {
var swarm = data;
- return Stack.create({ method: 'repository' }, { Name: name, SwarmID: swarm.Id, GitRepository: gitRepository, PathInRepository: pathInRepository }).$promise;
+ var payload = {
+ Name: name,
+ SwarmID: swarm.Id,
+ GitRepository: gitRepository,
+ PathInRepository: pathInRepository,
+ Env: env
+ };
+ return Stack.create({ method: 'repository' }, payload).$promise;
})
.then(function success(data) {
deferred.resolve(data);
@@ -136,13 +149,13 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.createStackFromFileUpload = function(name, stackFile) {
+ service.createStackFromFileUpload = function(name, stackFile, env) {
var deferred = $q.defer();
SwarmService.swarm()
.then(function success(data) {
var swarm = data;
- return FileUploadService.createStack(name, swarm.Id, stackFile);
+ return FileUploadService.createStack(name, swarm.Id, stackFile, env);
})
.then(function success(data) {
deferred.resolve(data.data);
@@ -154,8 +167,8 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.updateStack = function(id, stackFile) {
- return Stack.update({ id: id, StackFileContent: stackFile }).$promise;
+ service.updateStack = function(id, stackFile, env) {
+ return Stack.update({ id: id, StackFileContent: stackFile, Env: env }).$promise;
};
return service;
diff --git a/app/services/codeMirror.js b/app/services/codeMirror.js
index b5807f87e..e4cf3b821 100644
--- a/app/services/codeMirror.js
+++ b/app/services/codeMirror.js
@@ -2,8 +2,11 @@ angular.module('portainer.services')
.factory('CodeMirrorService', function CodeMirrorService() {
'use strict';
- var codeMirrorOptions = {
- lineNumbers: true,
+ var codeMirrorGenericOptions = {
+ lineNumbers: true
+ };
+
+ var codeMirrorYAMLOptions = {
mode: 'text/x-yaml',
gutters: ['CodeMirror-lint-markers'],
lint: true
@@ -11,8 +14,18 @@ angular.module('portainer.services')
var service = {};
- service.applyCodeMirrorOnElement = function(element) {
- var cm = CodeMirror.fromTextArea(element, codeMirrorOptions);
+ service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
+ var options = codeMirrorGenericOptions;
+
+ if (yamlLint) {
+ options = codeMirrorYAMLOptions;
+ }
+
+ if (readOnly) {
+ options.readOnly = true;
+ }
+
+ var cm = CodeMirror.fromTextArea(element, options);
cm.setSize('100%', 500);
return cm;
};
diff --git a/app/services/docker/configService.js b/app/services/docker/configService.js
new file mode 100644
index 000000000..530c689e7
--- /dev/null
+++ b/app/services/docker/configService.js
@@ -0,0 +1,61 @@
+angular.module('portainer.services')
+.factory('ConfigService', ['$q', 'Config', function ConfigServiceFactory($q, Config) {
+ 'use strict';
+ var service = {};
+
+ service.config = function(configId) {
+ var deferred = $q.defer();
+
+ Config.get({id: configId}).$promise
+ .then(function success(data) {
+ var config = new ConfigViewModel(data);
+ deferred.resolve(config);
+ })
+ .catch(function error(err) {
+ deferred.reject({ msg: 'Unable to retrieve config details', err: err });
+ });
+
+ return deferred.promise;
+ };
+
+ service.configs = function() {
+ var deferred = $q.defer();
+
+ Config.query({}).$promise
+ .then(function success(data) {
+ var configs = data.map(function (item) {
+ return new ConfigViewModel(item);
+ });
+ deferred.resolve(configs);
+ })
+ .catch(function error(err) {
+ deferred.reject({ msg: 'Unable to retrieve configs', err: err });
+ });
+
+ return deferred.promise;
+ };
+
+ service.remove = function(configId) {
+ var deferred = $q.defer();
+
+ Config.remove({ id: configId }).$promise
+ .then(function success(data) {
+ if (data.message) {
+ deferred.reject({ msg: data.message });
+ } else {
+ deferred.resolve();
+ }
+ })
+ .catch(function error(err) {
+ deferred.reject({ msg: 'Unable to remove config', err: err });
+ });
+
+ return deferred.promise;
+ };
+
+ service.create = function(config) {
+ return Config.create(config).$promise;
+ };
+
+ return service;
+}]);
diff --git a/app/services/docker/containerService.js b/app/services/docker/containerService.js
index 21bd1c295..2b09aedfa 100644
--- a/app/services/docker/containerService.js
+++ b/app/services/docker/containerService.js
@@ -20,8 +20,7 @@ angular.module('portainer.services')
service.containers = function(all, filters) {
var deferred = $q.defer();
-
- Container.query({ all: all, filters: filters ? filters : {} }).$promise
+ Container.query({ all : all, filters: filters }).$promise
.then(function success(data) {
var containers = data.map(function (item) {
return new ContainerViewModel(item);
@@ -140,18 +139,11 @@ angular.module('portainer.services')
};
service.containerTop = function(id) {
- var deferred = $q.defer();
+ return Container.top({id: id}).$promise;
+ };
- Container.top({id: id}).$promise
- .then(function success(data) {
- var containerTop = data;
- deferred.resolve(containerTop);
- })
- .catch(function error(err) {
- deferred.reject(err);
- });
-
- return deferred.promise;
+ service.inspect = function(id) {
+ return Container.inspect({id: id}).$promise;
};
return service;
diff --git a/app/services/fileUpload.js b/app/services/fileUpload.js
index 0f3f46643..da121eec6 100644
--- a/app/services/fileUpload.js
+++ b/app/services/fileUpload.js
@@ -8,9 +8,9 @@ angular.module('portainer.services')
return Upload.upload({ url: url, data: { file: file }});
}
- service.createStack = function(stackName, swarmId, file) {
+ service.createStack = function(stackName, swarmId, file, env) {
var endpointID = EndpointProvider.endpointID();
- return Upload.upload({ url: 'api/endpoints/' + endpointID + '/stacks?method=file', data: { file: file, Name: stackName, SwarmID: swarmId } });
+ return Upload.upload({ url: 'api/endpoints/' + endpointID + '/stacks?method=file', data: { file: file, Name: stackName, SwarmID: swarmId, Env: Upload.json(env) } });
};
service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {
diff --git a/app/services/templateService.js b/app/services/templateService.js
index a28e866ea..89694da59 100644
--- a/app/services/templateService.js
+++ b/app/services/templateService.js
@@ -9,7 +9,9 @@ angular.module('portainer.services')
.then(function success(data) {
var templates = data.map(function (tpl, idx) {
var template;
- if (key === 'linuxserver.io') {
+ if (tpl.type === 'stack') {
+ template = new StackTemplateViewModel(tpl);
+ } else if (tpl.type === 'container' && key === 'linuxserver.io') {
template = new TemplateLSIOViewModel(tpl);
} else {
template = new TemplateViewModel(tpl);
@@ -40,6 +42,7 @@ angular.module('portainer.services')
configuration.HostConfig.NetworkMode = network.Name;
configuration.HostConfig.Privileged = template.Privileged;
configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy };
+ configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : [];
configuration.name = containerName;
configuration.Hostname = containerName;
configuration.Image = template.Image;
diff --git a/assets/css/app.css b/assets/css/app.css
index 9fda8ca7a..3f6979afb 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -7,10 +7,6 @@ html, body, #content-wrapper, .page-content, #view {
white-space: normal !important;
}
-.btn-group button {
- margin: 3px;
-}
-
.messages {
max-height: 50px;
overflow-x: hidden;
@@ -282,13 +278,19 @@ a[ng-click]{
}
ul.sidebar {
- bottom: 40px;
+ position: relative;
+ overflow: hidden;
+ flex-shrink: 0;
}
ul.sidebar .sidebar-title {
height: auto;
}
+ul.sidebar .sidebar-list a {
+ font-size: 14px;
+}
+
ul.sidebar .sidebar-list a.active {
color: #fff;
text-indent: 22px;
@@ -296,7 +298,32 @@ ul.sidebar .sidebar-list a.active {
background: #2d3e63;
}
-.sidebar-footer .logo {
+.sidebar-header {
+ height: 60px;
+ list-style: none;
+ text-indent: 20px;
+ font-size: 18px;
+ background: #2d3e63;
+}
+
+.sidebar-header a { color: #fff; }
+.sidebar-header a:hover {text-decoration: none; }
+
+.sidebar-header .menu-icon {
+ float: right;
+ padding-right: 28px;
+ line-height: 60px;
+}
+
+#page-wrapper:not(.open) .sidebar-footer-content {
+ display: none;
+}
+
+.sidebar-footer-content {
+ text-align: center;
+}
+
+.sidebar-footer-content .logo {
width: 100%;
max-width: 100px;
height: 100%;
@@ -304,12 +331,26 @@ ul.sidebar .sidebar-list a.active {
margin: 2px 0 2px 20px;
}
-.sidebar-footer .version {
+.sidebar-footer-content .version {
font-size: 11px;
margin: 11px 20px 0 7px;
color: #fff;
}
+#sidebar-wrapper {
+ display: flex;
+ flex-flow: column;
+}
+
+.sidebar-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ overflow-y: auto;
+ overflow-x: hidden;
+ height: 100%;
+}
+
#image-layers .btn{
padding: 0;
}
@@ -331,6 +372,46 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
background: #2d3e63;
}
+@media (max-height: 683px) {
+ ul.sidebar .sidebar-title {
+ line-height: 28px;
+ }
+ ul.sidebar .sidebar-title .form-control {
+ height: 28px;
+ padding: 4px 8px;
+ }
+ ul.sidebar .sidebar-list {
+ height: 28px;
+ }
+ ul.sidebar .sidebar-list a, ul.sidebar .sidebar-list .sidebar-sublist a {
+ font-size: 12px;
+ line-height: 28px;
+ }
+ ul.sidebar .sidebar-list .menu-icon {
+ line-height: 28px;
+ }
+}
+
+@media(min-height: 684px) and (max-height: 850px) {
+ ul.sidebar .sidebar-title {
+ line-height: 30px;
+ }
+ ul.sidebar .sidebar-title .form-control {
+ height: 30px;
+ padding: 5px 10px;
+ }
+ ul.sidebar .sidebar-list {
+ height: 30px;
+ }
+ ul.sidebar .sidebar-list a, ul.sidebar .sidebar-list .sidebar-sublist a {
+ font-size: 12px;
+ line-height: 30px;
+ }
+ ul.sidebar .sidebar-list .menu-icon {
+ line-height: 30px;
+ }
+}
+
@media(min-width: 768px) and (max-width: 992px) {
.margin-sm-top {
margin-top: 5px;
@@ -615,3 +696,20 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
font-family: monospace;
font-weight: 600;
}
+
+/* json-tree */
+
+json-tree {
+ font-size: 13px;
+ color: #30426a;
+}
+json-tree .key {
+ color: #738bc0;
+ padding-right: 5px;
+}
+
+json-tree .branch-preview {
+ font-style: normal;
+ font-size: 11px;
+ opacity: .5;
+}
diff --git a/assets/ico/android-chrome-192x192.png b/assets/ico/android-chrome-192x192.png
index abf81c516..6d31b95f2 100644
Binary files a/assets/ico/android-chrome-192x192.png and b/assets/ico/android-chrome-192x192.png differ
diff --git a/assets/ico/android-chrome-256x256.png b/assets/ico/android-chrome-256x256.png
index 8770d7d3b..a7eb8da10 100644
Binary files a/assets/ico/android-chrome-256x256.png and b/assets/ico/android-chrome-256x256.png differ
diff --git a/assets/ico/apple-touch-icon.png b/assets/ico/apple-touch-icon.png
index 6ca3086cd..b5aa4b2e7 100644
Binary files a/assets/ico/apple-touch-icon.png and b/assets/ico/apple-touch-icon.png differ
diff --git a/assets/ico/browserconfig.xml b/assets/ico/browserconfig.xml
index f9aefe5b5..44751e9d4 100644
--- a/assets/ico/browserconfig.xml
+++ b/assets/ico/browserconfig.xml
@@ -2,7 +2,7 @@
-
+
#2d89ef
diff --git a/assets/ico/favicon-16x16.png b/assets/ico/favicon-16x16.png
index 192339c24..239474fb2 100644
Binary files a/assets/ico/favicon-16x16.png and b/assets/ico/favicon-16x16.png differ
diff --git a/assets/ico/favicon-32x32.png b/assets/ico/favicon-32x32.png
index 25c2aeeb1..ab08671dc 100644
Binary files a/assets/ico/favicon-32x32.png and b/assets/ico/favicon-32x32.png differ
diff --git a/assets/ico/manifest.json b/assets/ico/manifest.json
index e753aeb6b..843e03e83 100644
--- a/assets/ico/manifest.json
+++ b/assets/ico/manifest.json
@@ -2,12 +2,12 @@
"name": "Portainer",
"icons": [
{
- "src": "/ico/android-chrome-192x192.png",
+ "src": "ico/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
- "src": "/ico/android-chrome-256x256.png",
+ "src": "ico/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
diff --git a/assets/ico/mstile-150x150.png b/assets/ico/mstile-150x150.png
index f3670168a..60643c1ac 100644
Binary files a/assets/ico/mstile-150x150.png and b/assets/ico/mstile-150x150.png differ
diff --git a/assets/images/logo.png b/assets/images/logo.png
index 82f5a35d4..f0ba92c53 100644
Binary files a/assets/images/logo.png and b/assets/images/logo.png differ
diff --git a/assets/images/logo_ico.png b/assets/images/logo_ico.png
index c233495c2..b4bfd2924 100644
Binary files a/assets/images/logo_ico.png and b/assets/images/logo_ico.png differ
diff --git a/assets/images/logo_small.png b/assets/images/logo_small.png
index 2cd37301a..636f43ab9 100644
Binary files a/assets/images/logo_small.png and b/assets/images/logo_small.png differ
diff --git a/bower.json b/bower.json
index bf9bd43fa..faa85bf65 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "portainer",
- "version": "1.15.0",
+ "version": "1.15.1",
"homepage": "https://github.com/portainer/portainer",
"authors": [
"Anthony Lapenna "
@@ -34,6 +34,7 @@
"angular-utils-pagination": "~0.11.1",
"angular-local-storage": "~0.5.2",
"angular-jwt": "~0.1.8",
+ "angular-json-tree": "1.0.1",
"angular-google-analytics": "~1.1.9",
"bootstrap": "~3.3.6",
"filesize": "~3.3.0",
diff --git a/build.sh b/build.sh
index 77271abe9..e8d603292 100755
--- a/build.sh
+++ b/build.sh
@@ -42,7 +42,7 @@ else
if [ `echo "$@" | cut -c1-4` == 'echo' ]; then
bash -c "$@";
else
- build_all 'linux-amd64 linux-arm linux-arm64 linux-ppc64le darwin-amd64 windows-amd64'
+ build_all 'linux-amd64 linux-arm linux-arm64 linux-ppc64le linux-s390x darwin-amd64 windows-amd64'
exit 0
fi
fi
diff --git a/codefresh.yml b/codefresh.yml
index 25413098b..c669dbaef 100644
--- a/codefresh.yml
+++ b/codefresh.yml
@@ -18,9 +18,17 @@ steps:
- grunt build-webapp
- mv api/cmd/portainer/portainer dist/
+ download_docker_binary:
+ image: busybox
+ working_directory: ${{build_frontend}}
+ commands:
+ - wget -O /tmp/docker-binaries.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${{DOCKER_VERSION}}.tgz
+ - tar -xf /tmp/docker-binaries.tgz -C /tmp
+ - mv /tmp/docker/docker dist/
+
build_image:
type: build
- working_directory: ${{build_frontend}}
+ working_directory: ${{download_docker_binary}}
dockerfile: ./build/linux/Dockerfile
image_name: portainer/portainer
tag: ${{CF_BRANCH}}
diff --git a/distribution/portainer.service b/distribution/portainer.service
new file mode 100644
index 000000000..557784441
--- /dev/null
+++ b/distribution/portainer.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=Portainer.io management ui
+After=docker.service
+Wants=docker.service
+Wants=docker-latest.service
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=3
+Environment=ASSETS=/usr/share/portainer
+Environment=DBFILES=/var/lib/portainer
+EnvironmentFile=-/etc/sysconfig/%p
+ExecStart=/usr/sbin/portainer -a $ASSETS -d $DBFILES
+
+[Install]
+WantedBy=multi-user.target
diff --git a/distribution/portainer.spec b/distribution/portainer.spec
new file mode 100644
index 000000000..a2d06e102
--- /dev/null
+++ b/distribution/portainer.spec
@@ -0,0 +1,96 @@
+Name: portainer
+Version: 1.15.1
+Release: 0
+License: Zlib
+Summary: A lightweight docker management UI
+Url: https://portainer.io
+Group: BLAH
+Source0: https://github.com/portainer/portainer/releases/download/%{version}/portainer-%{version}-linux-amd64.tar.gz
+Source1: portainer.service
+BuildRoot: %{_tmppath}/%{name}-%{version}-build
+%if 0%{?suse_version}
+BuildRequires: help2man
+%endif
+Requires: docker
+%{?systemd_requires}
+BuildRequires: systemd
+
+%description
+Portainer is a lightweight management UI which allows you to easily manage
+your different Docker environments (Docker hosts or Swarm clusters).
+Portainer is meant to be as simple to deploy as it is to use.
+It consists of a single container that can run on any Docker engine
+(can be deployed as Linux container or a Windows native container).
+Portainer allows you to manage your Docker containers, images, volumes,
+networks and more ! It is compatible with the standalone Docker engine and with Docker Swarm mode.
+
+%prep
+%setup -qn portainer
+
+%build
+%if 0%{?suse_version}
+help2man -N --no-discard-stderr ./portainer > portainer.1
+%endif
+
+%install
+# Create directory structure
+install -D -m 0755 portainer %{buildroot}%{_sbindir}/portainer
+install -d -m 0755 %{buildroot}%{_datadir}/portainer
+install -d -m 0755 %{buildroot}%{_localstatedir}/lib/portainer
+install -D -m 0644 %{S:1} %{buildroot}%{_unitdir}/portainer.service
+%if 0%{?suse_version}
+install -D -m 0644 portainer.1 %{buildroot}%{_mandir}/man1/portainer.1
+( cd %{buildroot}%{_sbindir} ; ln -s service rcportainer )
+%endif
+# populate
+# don't install docker binary with package use system wide installed one
+for src in css fonts ico images index.html js;do
+ cp -a $src %{buildroot}%{_datadir}/portainer/
+done
+
+%pre
+%if 0%{?suse_version}
+%service_add_pre portainer.service
+#%%else # this does not work on rhel 7?
+#%%systemd_pre portainer.service
+true
+%endif
+
+%preun
+%if 0%{?suse_version}
+%service_del_preun portainer.service
+%else
+%systemd_preun portainer.service
+%endif
+
+%post
+%if 0%{?suse_version}
+%service_add_post portainer.service
+%else
+%systemd_post portainer.service
+%endif
+
+%postun
+%if 0%{?suse_version}
+%service_del_postun portainer.service
+%else
+%systemd_postun_with_restart portainer.service
+%endif
+
+
+%files
+%defattr(-,root,root)
+%{_sbindir}/portainer
+%dir %{_datadir}/portainer
+%{_datadir}/portainer/css
+%{_datadir}/portainer/fonts
+%{_datadir}/portainer/ico
+%{_datadir}/portainer/images
+%{_datadir}/portainer/index.html
+%{_datadir}/portainer/js
+%dir %{_localstatedir}/lib/portainer/
+%{_unitdir}/portainer.service
+%if 0%{?suse_version}
+%{_mandir}/man1/portainer.1*
+%{_sbindir}/rcportainer
+%endif
\ No newline at end of file
diff --git a/gruntfile.js b/gruntfile.js
index d4ff5fede..d7ae868d2 100644
--- a/gruntfile.js
+++ b/gruntfile.js
@@ -1,11 +1,15 @@
var autoprefixer = require('autoprefixer');
var cssnano = require('cssnano');
var loadGruntTasks = require('load-grunt-tasks');
+var os = require('os');
+var arch = os.arch();
+if ( arch === 'x64' ) arch = 'amd64';
module.exports = function (grunt) {
- loadGruntTasks(grunt);
- grunt.loadNpmTasks('gruntify-eslint');
+ loadGruntTasks(grunt, {
+ pattern: ['grunt-*', 'gruntify-*']
+ });
grunt.registerTask('default', ['eslint', 'build']);
grunt.registerTask('before-copy', [
@@ -33,8 +37,8 @@ module.exports = function (grunt) {
grunt.registerTask('build', [
'config:dev',
'clean:app',
- 'shell:buildBinary:linux:amd64',
- 'shell:downloadDockerBinary:linux:amd64',
+ 'shell:buildBinary:linux:' + arch,
+ 'shell:downloadDockerBinary:linux:' + arch,
'vendor:regular',
'html2js',
'useminPrepare:dev',
@@ -53,7 +57,7 @@ module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
- distdir: 'dist',
+ distdir: 'dist/public',
shippedDockerVersion: '17.09.0-ce',
pkg: grunt.file.readJSON('package.json'),
config: {
@@ -68,8 +72,8 @@ module.exports = function (grunt) {
css: ['assets/css/app.css']
},
clean: {
- all: ['<%= distdir %>/*'],
- app: ['<%= distdir %>/*', '!<%= distdir %>/portainer*', '!<%= distdir %>/docker*'],
+ all: ['<%= distdir %>/../*'],
+ app: ['<%= distdir %>/*', '!<%= distdir %>/../portainer*', '!<%= distdir %>/../docker*'],
tmpl: ['<%= distdir %>/templates'],
tmp: ['<%= distdir %>/js/*', '!<%= distdir %>/js/app.*.js', '<%= distdir %>/css/*', '!<%= distdir %>/css/app.*.css']
},
@@ -89,7 +93,8 @@ module.exports = function (grunt) {
release: {
src: '<%= src.html %>',
options: {
- root: '<%= distdir %>'
+ root: '<%= distdir %>',
+ dest: '<%= distdir %>'
}
}
},
@@ -183,7 +188,7 @@ module.exports = function (grunt) {
run: {
command: [
'docker rm -f portainer',
- 'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer-linux-amd64 --no-analytics -a /app'
+ 'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer-linux-' + arch + ' --no-analytics'
].join(';')
},
downloadDockerBinary: {
diff --git a/index.html b/index.html
index f120e0164..1949e5459 100644
--- a/index.html
+++ b/index.html
@@ -24,13 +24,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 0f48ef054..eb476ea2a 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
- "version": "1.15.0",
+ "version": "1.15.1",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
diff --git a/vendor.yml b/vendor.yml
index c052c018d..134914a7d 100644
--- a/vendor.yml
+++ b/vendor.yml
@@ -47,6 +47,7 @@ css:
- bower_components/angularjs-slider/dist/rzslider.css
- bower_components/codemirror/lib/codemirror.css
- bower_components/codemirror/addon/lint/lint.css
+ - bower_components/angular-json-tree/dist/angular-json-tree.css
minified:
- bower_components/bootstrap/dist/css/bootstrap.min.css
- bower_components/rdash-ui/dist/css/rdash.min.css
@@ -58,6 +59,7 @@ css:
- bower_components/angularjs-slider/dist/rzslider.min.css
- bower_components/codemirror/lib/codemirror.css
- bower_components/codemirror/addon/lint/lint.css
+ - bower_components/angular-json-tree/dist/angular-json-tree.css
angular:
regular:
- bower_components/angular/angular.js
@@ -74,6 +76,7 @@ angular:
- bower_components/ng-file-upload/ng-file-upload.js
- bower_components/angularjs-slider/dist/rzslider.js
- bower_components/angular-multi-select/isteven-multi-select.js
+ - bower_components/angular-json-tree/dist/angular-json-tree.js
minified:
- bower_components/angular/angular.min.js
- bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js
@@ -89,3 +92,4 @@ angular:
- bower_components/ng-file-upload/ng-file-upload.min.js
- bower_components/angularjs-slider/dist/rzslider.min.js
- bower_components/angular-multi-select/isteven-multi-select.js
+ - bower_components/angular-json-tree/dist/angular-json-tree.min.js