mirror of https://github.com/portainer/portainer
commit
4839c5f313
|
@ -36,6 +36,7 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||||
|
|
||||||
// Checking if a mount directory exists is broken with Go on Windows.
|
// Checking if a mount directory exists is broken with Go on Windows.
|
||||||
// This will need to be reviewed after the issue has been fixed in Go.
|
// This will need to be reviewed after the issue has been fixed in Go.
|
||||||
|
// See: https://github.com/portainer/portainer/issues/474
|
||||||
// err := createDirectoryIfNotExist(dataStorePath, 0755)
|
// err := createDirectoryIfNotExist(dataStorePath, 0755)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// return nil, err
|
// return nil, err
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (server *Server) Start() error {
|
||||||
var settingsHandler = NewSettingsHandler(middleWareService)
|
var settingsHandler = NewSettingsHandler(middleWareService)
|
||||||
settingsHandler.settings = server.Settings
|
settingsHandler.settings = server.Settings
|
||||||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||||
templatesHandler.templatesURL = server.TemplatesURL
|
templatesHandler.containerTemplatesURL = server.TemplatesURL
|
||||||
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
|
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
|
||||||
dockerHandler.EndpointService = server.EndpointService
|
dockerHandler.EndpointService = server.EndpointService
|
||||||
var websocketHandler = NewWebSocketHandler()
|
var websocketHandler = NewWebSocketHandler()
|
||||||
|
|
|
@ -12,10 +12,14 @@ import (
|
||||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||||
type TemplatesHandler struct {
|
type TemplatesHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
templatesURL string
|
containerTemplatesURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
containerTemplatesURLLinuxServerIo = "http://tools.linuxserver.io/portainer.json"
|
||||||
|
)
|
||||||
|
|
||||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||||
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
||||||
h := &TemplatesHandler{
|
h := &TemplatesHandler{
|
||||||
|
@ -27,14 +31,30 @@ func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetTemplates handles GET requests on /templates
|
// handleGetTemplates handles GET requests on /templates?key=<key>
|
||||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
handleNotAllowed(w, []string{http.MethodGet})
|
handleNotAllowed(w, []string{http.MethodGet})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(handler.templatesURL)
|
key := r.FormValue("key")
|
||||||
|
if key == "" {
|
||||||
|
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var templatesURL string
|
||||||
|
if key == "containers" {
|
||||||
|
templatesURL = handler.containerTemplatesURL
|
||||||
|
} else if key == "linuxserver.io" {
|
||||||
|
templatesURL = containerTemplatesURLLinuxServerIo
|
||||||
|
} else {
|
||||||
|
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(templatesURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
|
|
@ -176,7 +176,7 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of Portainer API.
|
// APIVersion is the version number of Portainer API.
|
||||||
APIVersion = "1.12.2"
|
APIVersion = "1.12.3"
|
||||||
// DBVersion is the version number of Portainer database.
|
// DBVersion is the version number of Portainer database.
|
||||||
DBVersion = 1
|
DBVersion = 1
|
||||||
)
|
)
|
||||||
|
|
23
app/app.js
23
app/app.js
|
@ -456,6 +456,27 @@ angular.module('portainer', [
|
||||||
})
|
})
|
||||||
.state('templates', {
|
.state('templates', {
|
||||||
url: '/templates/',
|
url: '/templates/',
|
||||||
|
params: {
|
||||||
|
key: 'containers',
|
||||||
|
hide_descriptions: false
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
"content@": {
|
||||||
|
templateUrl: 'app/components/templates/templates.html',
|
||||||
|
controller: 'TemplatesController'
|
||||||
|
},
|
||||||
|
"sidebar@": {
|
||||||
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
controller: 'SidebarController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('templates_linuxserver', {
|
||||||
|
url: '^/templates/linuxserver.io',
|
||||||
|
params: {
|
||||||
|
key: 'linuxserver.io',
|
||||||
|
hide_descriptions: true
|
||||||
|
},
|
||||||
views: {
|
views: {
|
||||||
"content@": {
|
"content@": {
|
||||||
templateUrl: 'app/components/templates/templates.html',
|
templateUrl: 'app/components/templates/templates.html',
|
||||||
|
@ -573,4 +594,4 @@ angular.module('portainer', [
|
||||||
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
||||||
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
||||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||||
.constant('UI_VERSION', 'v1.12.2');
|
.constant('UI_VERSION', 'v1.12.3');
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
Restrict external access to the network
|
Restrict external access to the network
|
||||||
</label>
|
</label>
|
||||||
<label class="switch" style="margin-left: 20px;">
|
<label class="switch" style="margin-left: 20px;">
|
||||||
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
|
<input type="checkbox" ng-model="config.Internal"><i></i>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -83,8 +83,15 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Authentication,
|
||||||
function preparePortsConfig(config, input) {
|
function preparePortsConfig(config, input) {
|
||||||
var ports = [];
|
var ports = [];
|
||||||
input.Ports.forEach(function (binding) {
|
input.Ports.forEach(function (binding) {
|
||||||
if (binding.PublishedPort && binding.TargetPort) {
|
var port = {
|
||||||
ports.push({ PublishedPort: +binding.PublishedPort, TargetPort: +binding.TargetPort, Protocol: binding.Protocol });
|
Protocol: binding.Protocol
|
||||||
|
};
|
||||||
|
if (binding.TargetPort) {
|
||||||
|
port.TargetPort = +binding.TargetPort;
|
||||||
|
if (binding.PublishedPort) {
|
||||||
|
port.PublishedPort = +binding.PublishedPort;
|
||||||
|
}
|
||||||
|
ports.push(port);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
config.EndpointSpec.Ports = ports;
|
config.EndpointSpec.Ports = ports;
|
||||||
|
|
|
@ -239,7 +239,7 @@
|
||||||
<!-- container-path -->
|
<!-- container-path -->
|
||||||
<div class="input-group input-group-sm col-sm-6">
|
<div class="input-group input-group-sm col-sm-6">
|
||||||
<span class="input-group-addon">container</span>
|
<span class="input-group-addon">container</span>
|
||||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/in/container">
|
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container">
|
||||||
</div>
|
</div>
|
||||||
<!-- !container-path -->
|
<!-- !container-path -->
|
||||||
<!-- volume-type -->
|
<!-- volume-type -->
|
||||||
|
@ -261,7 +261,7 @@
|
||||||
<!-- volume -->
|
<!-- volume -->
|
||||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
|
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
|
||||||
<span class="input-group-addon">volume</span>
|
<span class="input-group-addon">volume</span>
|
||||||
<select class="form-control" ng-model="volume.Target">
|
<select class="form-control" ng-model="volume.Source">
|
||||||
<option selected disabled hidden value="">Select a volume</option>
|
<option selected disabled hidden value="">Select a volume</option>
|
||||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -270,7 +270,7 @@
|
||||||
<!-- bind -->
|
<!-- bind -->
|
||||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
|
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
|
||||||
<span class="input-group-addon">host</span>
|
<span class="input-group-addon">host</span>
|
||||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/on/host">
|
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
|
||||||
</div>
|
</div>
|
||||||
<!-- !bind -->
|
<!-- !bind -->
|
||||||
<!-- read-only -->
|
<!-- read-only -->
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
angular.module('createVolume', [])
|
angular.module('createVolume', [])
|
||||||
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'ResourceControlService', 'Authentication', 'Messages',
|
.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'InfoService', 'ResourceControlService', 'Authentication', 'Messages',
|
||||||
function ($scope, $state, Volume, ResourceControlService, Authentication, Messages) {
|
function ($scope, $state, VolumeService, InfoService, ResourceControlService, Authentication, Messages) {
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
|
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
|
||||||
|
Driver: 'local',
|
||||||
DriverOptions: []
|
DriverOptions: []
|
||||||
};
|
};
|
||||||
|
$scope.availableVolumeDrivers = [];
|
||||||
$scope.config = {
|
|
||||||
Driver: 'local'
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.addDriverOption = function() {
|
$scope.addDriverOption = function() {
|
||||||
$scope.formValues.DriverOptions.push({ name: '', value: '' });
|
$scope.formValues.DriverOptions.push({ name: '', value: '' });
|
||||||
|
@ -19,52 +17,51 @@ function ($scope, $state, Volume, ResourceControlService, Authentication, Messag
|
||||||
$scope.formValues.DriverOptions.splice(index, 1);
|
$scope.formValues.DriverOptions.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function createVolume(config) {
|
|
||||||
$('#createVolumeSpinner').show();
|
|
||||||
Volume.create(config, function (d) {
|
|
||||||
if (d.message) {
|
|
||||||
$('#createVolumeSpinner').hide();
|
|
||||||
Messages.error('Unable to create volume', {}, d.message);
|
|
||||||
} else {
|
|
||||||
if ($scope.formValues.Ownership === 'private') {
|
|
||||||
ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, d.Name)
|
|
||||||
.then(function success() {
|
|
||||||
Messages.send("Volume created", d.Name);
|
|
||||||
$('#createVolumeSpinner').hide();
|
|
||||||
$state.go('volumes', {}, {reload: true});
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
$('#createVolumeSpinner').hide();
|
|
||||||
Messages.error("Failure", err, 'Unable to apply resource control on volume');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Messages.send("Volume created", d.Name);
|
|
||||||
$('#createVolumeSpinner').hide();
|
|
||||||
$state.go('volumes', {}, {reload: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, function (e) {
|
|
||||||
$('#createVolumeSpinner').hide();
|
|
||||||
Messages.error("Failure", e, 'Unable to create volume');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareDriverOptions(config) {
|
|
||||||
var options = {};
|
|
||||||
$scope.formValues.DriverOptions.forEach(function (option) {
|
|
||||||
options[option.name] = option.value;
|
|
||||||
});
|
|
||||||
config.DriverOpts = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareConfiguration() {
|
|
||||||
var config = angular.copy($scope.config);
|
|
||||||
prepareDriverOptions(config);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.create = function () {
|
$scope.create = function () {
|
||||||
var config = prepareConfiguration();
|
$('#createVolumeSpinner').show();
|
||||||
createVolume(config);
|
|
||||||
|
var name = $scope.formValues.Name;
|
||||||
|
var driver = $scope.formValues.Driver;
|
||||||
|
var driverOptions = $scope.formValues.DriverOptions;
|
||||||
|
var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions);
|
||||||
|
|
||||||
|
VolumeService.createVolume(volumeConfiguration)
|
||||||
|
.then(function success(data) {
|
||||||
|
if ($scope.formValues.Ownership === 'private') {
|
||||||
|
ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, data.Name)
|
||||||
|
.then(function success() {
|
||||||
|
Messages.send("Volume created", data.Name);
|
||||||
|
$state.go('volumes', {}, {reload: true});
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Messages.error("Failure", err, 'Unable to apply resource control on volume');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Messages.send("Volume created", data.Name);
|
||||||
|
$state.go('volumes', {}, {reload: true});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Messages.error('Failure', err, 'Unable to create volume');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#createVolumeSpinner').hide();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
$('#loadingViewSpinner').show();
|
||||||
|
InfoService.getVolumePlugins()
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.availableVolumeDrivers = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Messages.error("Failure", err, 'Unable to retrieve volume plugin information');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#loadingViewSpinner').hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title="Create volume"></rd-header-title>
|
<rd-header-title title="Create volume">
|
||||||
|
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||||
|
</rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="volumes">Volumes</a> > Add volume
|
<a ui-sref="volumes">Volumes</a> > Add volume
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
|
@ -14,7 +16,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. myVolume">
|
<input type="text" class="form-control" ng-model="formValues.Name" id="volume_name" placeholder="e.g. myVolume">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
|
@ -25,7 +27,10 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
|
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
<input type="text" class="form-control" ng-model="config.Driver" id="volume_driver" placeholder="e.g. driverName">
|
<select class="form-control" ng-options="driver for driver in availableVolumeDrivers" ng-model="formValues.Driver" ng-if="availableVolumeDrivers.length > 0">
|
||||||
|
<option disabled hidden value="">Select a driver</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.Driver" id="volume_driver" placeholder="e.g. driverName" ng-if="availableVolumeDrivers.length === 0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !driver-input -->
|
<!-- !driver-input -->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('service', [])
|
angular.module('service', [])
|
||||||
.controller('ServiceController', ['$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
|
.controller('ServiceController', ['$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination', 'ModalService',
|
||||||
function ($scope, $stateParams, $state, $location, $anchorScroll, Service, ServiceHelper, Task, Node, Messages, Pagination) {
|
function ($scope, $stateParams, $state, $location, $anchorScroll, Service, ServiceHelper, Task, Node, Messages, Pagination, ModalService) {
|
||||||
|
|
||||||
$scope.state = {};
|
$scope.state = {};
|
||||||
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
|
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
|
||||||
|
@ -197,6 +197,13 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi
|
||||||
MaxAttempts: service.RestartMaxAttempts,
|
MaxAttempts: service.RestartMaxAttempts,
|
||||||
Window: service.RestartWindow
|
Window: service.RestartWindow
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.Ports.forEach(function (binding) {
|
||||||
|
if (binding.PublishedPort === null || binding.PublishedPort === '') {
|
||||||
|
delete binding.PublishedPort;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
config.EndpointSpec = {
|
config.EndpointSpec = {
|
||||||
Mode: config.EndpointSpec.Mode || 'vip',
|
Mode: config.EndpointSpec.Mode || 'vip',
|
||||||
Ports: service.Ports
|
Ports: service.Ports
|
||||||
|
@ -213,8 +220,17 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.removeService = function() {
|
||||||
|
ModalService.confirmDeletion(
|
||||||
|
'Do you want to delete this service? All the containers associated to this service will be removed too.',
|
||||||
|
function onConfirm(confirmed) {
|
||||||
|
if(!confirmed) { return; }
|
||||||
|
removeService();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.removeService = function removeService() {
|
function removeService() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
Service.remove({id: $stateParams.id}, function (d) {
|
Service.remove({id: $stateParams.id}, function (d) {
|
||||||
if (d.message) {
|
if (d.message) {
|
||||||
|
@ -229,7 +245,7 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi
|
||||||
$('#loadingViewSpinner').hide();
|
$('#loadingViewSpinner').hide();
|
||||||
Messages.error("Failure", e, "Unable to remove service");
|
Messages.error("Failure", e, "Unable to remove service");
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
function translateServiceArrays(service) {
|
function translateServiceArrays(service) {
|
||||||
service.ServiceSecrets = service.Secrets;
|
service.ServiceSecrets = service.Secrets;
|
||||||
|
|
|
@ -131,10 +131,10 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!services">
|
<tr ng-if="!services">
|
||||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
<td colspan="7" class="text-center text-muted">Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="services.length == 0">
|
<tr ng-if="services.length == 0">
|
||||||
<td colspan="5" class="text-center text-muted">No services available.</td>
|
<td colspan="7" class="text-center text-muted">No services available.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -68,7 +68,17 @@ function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pa
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.removeAction = function () {
|
$scope.removeAction = function() {
|
||||||
|
ModalService.confirmDeletion(
|
||||||
|
'Do you want to delete the selected service(s)? All the containers associated to the selected service(s) will be removed too.',
|
||||||
|
function onConfirm(confirmed) {
|
||||||
|
if(!confirmed) { return; }
|
||||||
|
removeServices();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function removeServices() {
|
||||||
$('#loadServicesSpinner').show();
|
$('#loadServicesSpinner').show();
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
var complete = function () {
|
var complete = function () {
|
||||||
|
@ -108,7 +118,11 @@ function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pa
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// $scope.removeAction = function () {
|
||||||
|
//
|
||||||
|
// };
|
||||||
|
|
||||||
function mapUsersToServices(users) {
|
function mapUsersToServices(users) {
|
||||||
angular.forEach($scope.services, function (service) {
|
angular.forEach($scope.services, function (service) {
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
||||||
|
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')">
|
||||||
|
<a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||||
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
||||||
|
|
|
@ -14,17 +14,13 @@
|
||||||
</rd-widget-custom-header>
|
</rd-widget-custom-header>
|
||||||
<rd-widget-body classes="padding">
|
<rd-widget-body classes="padding">
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
<!-- description -->
|
||||||
|
<div class="form-group" ng-if="state.selectedTemplate.Description">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
<span class="small" style="margin-left: 5px;">{{ state.selectedTemplate.Description }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
|
||||||
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- !description -->
|
||||||
<!-- name-and-network-inputs -->
|
<!-- name-and-network-inputs -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
|
@ -61,8 +57,8 @@
|
||||||
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
|
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
|
||||||
</label>
|
</label>
|
||||||
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
|
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
|
||||||
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'">Private</label>
|
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">Private</label>
|
||||||
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'">Public</label>
|
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">Public</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -200,6 +196,13 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||||
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
<span class="small text-muted" style="margin-left: 10px" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||||
|
When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.
|
||||||
|
</span>
|
||||||
|
<span ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" style="margin-left: 10px">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
<span class="small text-muted" style="margin-left: 5px;">App templates cannot be deployed as Swarm Mode services for the moment. You can still use them to quickly deploy containers on the Docker host.</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -228,7 +231,7 @@
|
||||||
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
||||||
<img class="logo" ng-src="{{ tpl.Logo }}" />
|
<img class="logo" ng-src="{{ tpl.Logo }}" />
|
||||||
<div class="title">{{ tpl.Title }}</div>
|
<div class="title">{{ tpl.Title }}</div>
|
||||||
<div class="description">{{ tpl.Description }}</div>
|
<div class="description" ng-if="tpl.Description && !state.hideDescriptions">{{ tpl.Description }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="!templates" class="text-center text-muted">
|
<div ng-if="!templates" class="text-center text-muted">
|
||||||
Loading...
|
Loading...
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
angular.module('templates', [])
|
angular.module('templates', [])
|
||||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', 'ResourceControlService', 'Authentication',
|
.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', 'ResourceControlService', 'Authentication',
|
||||||
function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination, ResourceControlService, Authentication) {
|
function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination, ResourceControlService, Authentication) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
showAdvancedOptions: false,
|
showAdvancedOptions: false,
|
||||||
|
hideDescriptions: $stateParams.hide_descriptions,
|
||||||
pagination_count: Pagination.getPaginationCount('templates')
|
pagination_count: Pagination.getPaginationCount('templates')
|
||||||
};
|
};
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
|
@ -122,7 +123,11 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container
|
||||||
function filterNetworksBasedOnProvider(networks) {
|
function filterNetworksBasedOnProvider(networks) {
|
||||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||||
if (endpointProvider === 'DOCKER_SWARM' || endpointProvider === 'DOCKER_SWARM_MODE') {
|
if (endpointProvider === 'DOCKER_SWARM' || endpointProvider === 'DOCKER_SWARM_MODE') {
|
||||||
networks = NetworkService.filterGlobalNetworks(networks);
|
if (endpointProvider === 'DOCKER_SWARM') {
|
||||||
|
networks = NetworkService.filterGlobalNetworks(networks);
|
||||||
|
} else {
|
||||||
|
networks = NetworkService.filterSwarmModeAttachableNetworks(networks);
|
||||||
|
}
|
||||||
$scope.globalNetworkCount = networks.length;
|
$scope.globalNetworkCount = networks.length;
|
||||||
NetworkService.addPredefinedLocalNetworks(networks);
|
NetworkService.addPredefinedLocalNetworks(networks);
|
||||||
}
|
}
|
||||||
|
@ -130,15 +135,20 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTemplates() {
|
function initTemplates() {
|
||||||
|
var templatesKey = $stateParams.key;
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$q.all({
|
$q.all({
|
||||||
templates: TemplateService.getTemplates(),
|
templates: TemplateService.getTemplates(templatesKey),
|
||||||
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
||||||
networks: NetworkService.getNetworks(),
|
networks: NetworkService.getNetworks(),
|
||||||
volumes: VolumeService.getVolumes()
|
volumes: VolumeService.getVolumes()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.templates = data.templates;
|
var templates = data.templates;
|
||||||
|
if (templatesKey === 'linuxserver.io') {
|
||||||
|
templates = TemplateService.filterLinuxServerIOTemplates(templates);
|
||||||
|
}
|
||||||
|
$scope.templates = templates;
|
||||||
$scope.runningContainers = data.containers;
|
$scope.runningContainers = data.containers;
|
||||||
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
||||||
$scope.availableVolumes = data.volumes.Volumes;
|
$scope.availableVolumes = data.volumes.Volumes;
|
||||||
|
|
|
@ -93,5 +93,22 @@ angular.module('portainer.helpers')
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
helper.filterLinuxServerIOTemplates = function(templates) {
|
||||||
|
return templates.filter(function f(template) {
|
||||||
|
var valid = false;
|
||||||
|
if (template.Category) {
|
||||||
|
angular.forEach(template.Category, function(category) {
|
||||||
|
if (_.startsWith(category, 'Network')) {
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}).map(function(template, idx) {
|
||||||
|
template.index = idx;
|
||||||
|
return template;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return helper;
|
return helper;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
angular.module('portainer.helpers')
|
||||||
|
.factory('VolumeHelper', [function VolumeHelperFactory() {
|
||||||
|
'use strict';
|
||||||
|
var helper = {};
|
||||||
|
|
||||||
|
helper.createDriverOptions = function(optionArray) {
|
||||||
|
var options = {};
|
||||||
|
optionArray.forEach(function (option) {
|
||||||
|
options[option.name] = option.value;
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
return helper;
|
||||||
|
}]);
|
|
@ -53,8 +53,8 @@ function ServiceViewModel(data, runningTasks, nodes) {
|
||||||
this.Command = containerSpec.Command;
|
this.Command = containerSpec.Command;
|
||||||
this.Secrets = containerSpec.Secrets;
|
this.Secrets = containerSpec.Secrets;
|
||||||
}
|
}
|
||||||
if (data.Spec.EndpointSpec) {
|
if (data.Endpoint) {
|
||||||
this.Ports = data.Spec.EndpointSpec.Ports;
|
this.Ports = data.Endpoint.Ports;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Mounts = [];
|
this.Mounts = [];
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
function TemplateViewModel(data) {
|
function TemplateViewModel(data) {
|
||||||
this.Title = data.title;
|
this.Title = data.title;
|
||||||
this.Description = data.description;
|
this.Description = data.description;
|
||||||
|
this.Category = data.category;
|
||||||
this.Logo = data.logo;
|
this.Logo = data.logo;
|
||||||
this.Image = data.image;
|
this.Image = data.image;
|
||||||
this.Registry = data.registry ? data.registry : '';
|
this.Registry = data.registry ? data.registry : '';
|
||||||
|
|
|
@ -9,7 +9,7 @@ angular.module('portainer.services')
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var containers = data;
|
var containers = data;
|
||||||
if (hiddenLabels) {
|
if (hiddenLabels) {
|
||||||
containers = ContainerHelper.hideContainers(d, hiddenLabels);
|
containers = ContainerHelper.hideContainers(data, hiddenLabels);
|
||||||
}
|
}
|
||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
angular.module('portainer.services')
|
||||||
|
.factory('InfoService', ['$q', 'Info', function InfoServiceFactory($q, Info) {
|
||||||
|
'use strict';
|
||||||
|
var service = {};
|
||||||
|
|
||||||
|
service.getVolumePlugins = function() {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
Info.get({}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var plugins = data.Plugins.Volume;
|
||||||
|
deferred.resolve(plugins);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({msg: 'Unable to retrieve volume plugin information', err: err});
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}]);
|
|
@ -15,6 +15,14 @@ angular.module('portainer.services')
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.filterSwarmModeAttachableNetworks = function(networks) {
|
||||||
|
return networks.filter(function (network) {
|
||||||
|
if (network.Scope === 'swarm' && network.Attachable === true) {
|
||||||
|
return network;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
service.addPredefinedLocalNetworks = function(networks) {
|
service.addPredefinedLocalNetworks = function(networks) {
|
||||||
networks.push({Scope: "local", Name: "bridge"});
|
networks.push({Scope: "local", Name: "bridge"});
|
||||||
networks.push({Scope: "local", Name: "host"});
|
networks.push({Scope: "local", Name: "host"});
|
||||||
|
|
|
@ -3,9 +3,9 @@ angular.module('portainer.services')
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.getTemplates = function() {
|
service.getTemplates = function(key) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
Template.get().$promise
|
Template.get({key: key}).$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var templates = data.map(function (tpl, idx) {
|
var templates = data.map(function (tpl, idx) {
|
||||||
var template = new TemplateViewModel(tpl);
|
var template = new TemplateViewModel(tpl);
|
||||||
|
@ -20,6 +20,10 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.filterLinuxServerIOTemplates = function(templates) {
|
||||||
|
return TemplateHelper.filterLinuxServerIOTemplates(templates);
|
||||||
|
};
|
||||||
|
|
||||||
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
|
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
|
||||||
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
||||||
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
|
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
angular.module('portainer.services')
|
angular.module('portainer.services')
|
||||||
.factory('VolumeService', ['$q', 'Volume', function VolumeServiceFactory($q, Volume) {
|
.factory('VolumeService', ['$q', 'Volume', 'VolumeHelper', function VolumeServiceFactory($q, Volume, VolumeHelper) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
@ -7,27 +7,14 @@ angular.module('portainer.services')
|
||||||
return Volume.query({}).$promise;
|
return Volume.query({}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
function prepareVolumeQueries(template, containerConfig) {
|
service.createVolumeConfiguration = function(name, driver, driverOptions) {
|
||||||
var volumeQueries = [];
|
var volumeConfiguration = {
|
||||||
if (template.volumes) {
|
Name: name,
|
||||||
template.volumes.forEach(function (vol) {
|
Driver: driver,
|
||||||
volumeQueries.push(
|
DriverOpts: VolumeHelper.createDriverOptions(driverOptions)
|
||||||
Volume.create({}, function (d) {
|
};
|
||||||
if (d.message) {
|
return volumeConfiguration;
|
||||||
Messages.error("Unable to create volume", {}, d.message);
|
};
|
||||||
} else {
|
|
||||||
Messages.send("Volume created", d.Name);
|
|
||||||
containerConfig.Volumes[vol] = {};
|
|
||||||
containerConfig.HostConfig.Binds.push(d.Name + ':' + vol);
|
|
||||||
}
|
|
||||||
}, function (e) {
|
|
||||||
Messages.error("Failure", e, "Unable to create volume");
|
|
||||||
}).$promise
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return volumeQueries;
|
|
||||||
}
|
|
||||||
|
|
||||||
service.createVolume = function(volumeConfiguration) {
|
service.createVolume = function(volumeConfiguration) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
@ -45,9 +32,9 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.createVolumes = function(volumes) {
|
service.createVolumes = function(volumeConfigurations) {
|
||||||
var createVolumeQueries = volumes.map(function(volume) {
|
var createVolumeQueries = volumeConfigurations.map(function(volumeConfiguration) {
|
||||||
return service.createVolume(volume);
|
return service.createVolume(volumeConfiguration);
|
||||||
});
|
});
|
||||||
return $q.all(createVolumeQueries);
|
return $q.all(createVolumeQueries);
|
||||||
};
|
};
|
||||||
|
|
|
@ -301,6 +301,10 @@ ul.sidebar {
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.sidebar .sidebar-title {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
ul.sidebar .sidebar-list a.active {
|
ul.sidebar .sidebar-list a.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-indent: 22px;
|
text-indent: 22px;
|
||||||
|
@ -308,6 +312,19 @@ ul.sidebar .sidebar-list a.active {
|
||||||
background: #2d3e63;
|
background: #2d3e63;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||||
|
text-indent: 35px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #b2bfdc;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
|
color: #fff;
|
||||||
|
border-left: 3px solid #fff;
|
||||||
|
background: #2d3e63;
|
||||||
|
}
|
||||||
|
|
||||||
@media(min-width: 768px) and (max-width: 992px) {
|
@media(min-width: 768px) and (max-width: 992px) {
|
||||||
.margin-sm-top {
|
.margin-sm-top {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"version": "1.12.2",
|
"version": "1.12.3",
|
||||||
"homepage": "https://github.com/portainer/portainer",
|
"homepage": "https://github.com/portainer/portainer",
|
||||||
"authors": [
|
"authors": [
|
||||||
"Anthony Lapenna <anthony.lapenna at gmail dot com>"
|
"Anthony Lapenna <anthony.lapenna at gmail dot com>"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"author": "Portainer.io",
|
"author": "Portainer.io",
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"homepage": "http://portainer.io",
|
"homepage": "http://portainer.io",
|
||||||
"version": "1.12.2",
|
"version": "1.12.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:portainer/portainer.git"
|
"url": "git@github.com:portainer/portainer.git"
|
||||||
|
|
Loading…
Reference in New Issue