mirror of
https://github.com/portainer/portainer.git
synced 2025-08-08 07:15:23 +02:00
refactor(app): introduce new project structure for the frontend (#1623)
This commit is contained in:
parent
e6422a6d75
commit
27dceadba1
354 changed files with 1518 additions and 1755 deletions
523
app/docker/views/services/create/createServiceController.js
Normal file
523
app/docker/views/services/create/createServiceController.js
Normal file
|
@ -0,0 +1,523 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService',
|
||||
function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Image: '',
|
||||
Registry: {},
|
||||
Mode: 'replicated',
|
||||
Replicas: 1,
|
||||
Command: '',
|
||||
EntryPoint: '',
|
||||
WorkingDir: '',
|
||||
User: '',
|
||||
Env: [],
|
||||
Labels: [],
|
||||
ContainerLabels: [],
|
||||
Volumes: [],
|
||||
Network: '',
|
||||
ExtraNetworks: [],
|
||||
HostsEntries: [],
|
||||
Ports: [],
|
||||
Parallelism: 1,
|
||||
PlacementConstraints: [],
|
||||
PlacementPreferences: [],
|
||||
UpdateDelay: '0s',
|
||||
UpdateOrder: 'stop-first',
|
||||
FailureAction: 'pause',
|
||||
Secrets: [],
|
||||
Configs: [],
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
CpuLimit: 0,
|
||||
CpuReservation: 0,
|
||||
MemoryLimit: 0,
|
||||
MemoryReservation: 0,
|
||||
MemoryLimitUnit: 'MB',
|
||||
MemoryReservationUnit: 'MB',
|
||||
RestartCondition: 'any',
|
||||
RestartDelay: '5s',
|
||||
RestartMaxAttempts: 0,
|
||||
RestartWindow: '0s',
|
||||
LogDriverName: '',
|
||||
LogDriverOpts: []
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: '',
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.refreshSlider = function () {
|
||||
$timeout(function () {
|
||||
$scope.$broadcast('rzSliderForceRender');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function() {
|
||||
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
|
||||
};
|
||||
|
||||
$scope.removePortBinding = function(index) {
|
||||
$scope.formValues.Ports.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addExtraNetwork = function() {
|
||||
$scope.formValues.ExtraNetworks.push({ Name: '' });
|
||||
};
|
||||
|
||||
$scope.removeExtraNetwork = function(index) {
|
||||
$scope.formValues.ExtraNetworks.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addHostsEntry = function() {
|
||||
$scope.formValues.HostsEntries.push({});
|
||||
};
|
||||
|
||||
$scope.removeHostsEntry = function(index) {
|
||||
$scope.formValues.HostsEntries.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addVolume = function() {
|
||||
$scope.formValues.Volumes.push({ Source: '', Target: '', ReadOnly: false, Type: 'volume' });
|
||||
};
|
||||
|
||||
$scope.removeVolume = function(index) {
|
||||
$scope.formValues.Volumes.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addConfig = function() {
|
||||
$scope.formValues.Configs.push({});
|
||||
};
|
||||
|
||||
$scope.removeConfig = function(index) {
|
||||
$scope.formValues.Configs.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addSecret = function() {
|
||||
$scope.formValues.Secrets.push({ overrideTarget: false });
|
||||
};
|
||||
|
||||
$scope.removeSecret = function(index) {
|
||||
$scope.formValues.Secrets.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function() {
|
||||
$scope.formValues.Env.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeEnvironmentVariable = function(index) {
|
||||
$scope.formValues.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addPlacementConstraint = function() {
|
||||
$scope.formValues.PlacementConstraints.push({ key: '', operator: '==', value: '' });
|
||||
};
|
||||
|
||||
$scope.removePlacementConstraint = function(index) {
|
||||
$scope.formValues.PlacementConstraints.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addPlacementPreference = function() {
|
||||
$scope.formValues.PlacementPreferences.push({ strategy: 'spread', value: '' });
|
||||
};
|
||||
|
||||
$scope.removePlacementPreference = function(index) {
|
||||
$scope.formValues.PlacementPreferences.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ key: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addContainerLabel = function() {
|
||||
$scope.formValues.ContainerLabels.push({ key: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeContainerLabel = function(index) {
|
||||
$scope.formValues.ContainerLabels.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLogDriverOpt = function(value) {
|
||||
$scope.formValues.LogDriverOpts.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLogDriverOpt = function(index) {
|
||||
$scope.formValues.LogDriverOpts.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareImageConfig(config, input) {
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry.URL);
|
||||
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
}
|
||||
|
||||
function preparePortsConfig(config, input) {
|
||||
var ports = [];
|
||||
input.Ports.forEach(function (binding) {
|
||||
var port = {
|
||||
Protocol: binding.Protocol,
|
||||
PublishMode: binding.PublishMode
|
||||
};
|
||||
if (binding.TargetPort) {
|
||||
port.TargetPort = +binding.TargetPort;
|
||||
if (binding.PublishedPort) {
|
||||
port.PublishedPort = +binding.PublishedPort;
|
||||
}
|
||||
ports.push(port);
|
||||
}
|
||||
});
|
||||
config.EndpointSpec.Ports = ports;
|
||||
}
|
||||
|
||||
function prepareSchedulingConfig(config, input) {
|
||||
if (input.Mode === 'replicated') {
|
||||
config.Mode.Replicated = {
|
||||
Replicas: input.Replicas
|
||||
};
|
||||
} else {
|
||||
config.Mode.Global = {};
|
||||
}
|
||||
}
|
||||
|
||||
function commandToArray(cmd) {
|
||||
var tokens = [].concat.apply([], cmd.split('\'').map(function(v,i) {
|
||||
return i%2 ? v : v.split(' ');
|
||||
})).filter(Boolean);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function prepareCommandConfig(config, input) {
|
||||
if (input.EntryPoint) {
|
||||
config.TaskTemplate.ContainerSpec.Command = commandToArray(input.EntryPoint);
|
||||
}
|
||||
if (input.Command) {
|
||||
config.TaskTemplate.ContainerSpec.Args = commandToArray(input.Command);
|
||||
}
|
||||
if (input.User) {
|
||||
config.TaskTemplate.ContainerSpec.User = input.User;
|
||||
}
|
||||
if (input.WorkingDir) {
|
||||
config.TaskTemplate.ContainerSpec.Dir = input.WorkingDir;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareEnvConfig(config, input) {
|
||||
var env = [];
|
||||
input.Env.forEach(function (v) {
|
||||
if (v.name) {
|
||||
env.push(v.name + '=' + v.value);
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Env = env;
|
||||
}
|
||||
|
||||
function prepareLabelsConfig(config, input) {
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash(input.Labels);
|
||||
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(input.ContainerLabels);
|
||||
}
|
||||
|
||||
function createMountObjectFromVolume(volumeObject, target, readonly) {
|
||||
return {
|
||||
Target: target,
|
||||
Source: volumeObject.Id,
|
||||
Type: 'volume',
|
||||
ReadOnly: readonly,
|
||||
VolumeOptions: {
|
||||
Labels: volumeObject.Labels,
|
||||
DriverConfig: {
|
||||
Name: volumeObject.Driver,
|
||||
Options: volumeObject.Options
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function prepareVolumes(config, input) {
|
||||
input.Volumes.forEach(function (volume) {
|
||||
if (volume.Source && volume.Target) {
|
||||
if (volume.Type !== 'volume') {
|
||||
config.TaskTemplate.ContainerSpec.Mounts.push(volume);
|
||||
} else {
|
||||
var volumeObject = volume.Source;
|
||||
var mount = createMountObjectFromVolume(volumeObject, volume.Target, volume.ReadOnly);
|
||||
config.TaskTemplate.ContainerSpec.Mounts.push(mount);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function prepareNetworks(config, input) {
|
||||
var networks = [];
|
||||
if (input.Network) {
|
||||
networks.push({ Target: input.Network });
|
||||
}
|
||||
input.ExtraNetworks.forEach(function (network) {
|
||||
networks.push({ Target: network.Name });
|
||||
});
|
||||
config.Networks = _.uniqWith(networks, _.isEqual);
|
||||
}
|
||||
|
||||
function prepareHostsEntries(config, input) {
|
||||
var hostsEntries = [];
|
||||
if (input.HostsEntries) {
|
||||
input.HostsEntries.forEach(function (host_ip) {
|
||||
if (host_ip.value && host_ip.value.indexOf(':') && host_ip.value.split(':').length === 2) {
|
||||
var keyVal = host_ip.value.split(':');
|
||||
// Hosts file format, IP_address canonical_hostname
|
||||
hostsEntries.push(keyVal[1] + ' ' + keyVal[0]);
|
||||
}
|
||||
});
|
||||
if (hostsEntries.length > 0) {
|
||||
config.TaskTemplate.ContainerSpec.Hosts = hostsEntries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareUpdateConfig(config, input) {
|
||||
config.UpdateConfig = {
|
||||
Parallelism: input.Parallelism || 0,
|
||||
Delay: ServiceHelper.translateHumanDurationToNanos(input.UpdateDelay) || 0,
|
||||
FailureAction: input.FailureAction,
|
||||
Order: input.UpdateOrder
|
||||
};
|
||||
}
|
||||
|
||||
function prepareRestartPolicy(config, input) {
|
||||
config.TaskTemplate.RestartPolicy = {
|
||||
Condition: input.RestartCondition || 'any',
|
||||
Delay: ServiceHelper.translateHumanDurationToNanos(input.RestartDelay) || 5000000000,
|
||||
MaxAttempts: input.RestartMaxAttempts || 0,
|
||||
Window: ServiceHelper.translateHumanDurationToNanos(input.RestartWindow) || 0
|
||||
};
|
||||
}
|
||||
|
||||
function preparePlacementConfig(config, input) {
|
||||
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints);
|
||||
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(input.PlacementPreferences);
|
||||
}
|
||||
|
||||
function prepareConfigConfig(config, input) {
|
||||
if (input.Configs) {
|
||||
var configs = [];
|
||||
angular.forEach(input.Configs, function(config) {
|
||||
if (config.model) {
|
||||
var s = ConfigHelper.configConfig(config.model);
|
||||
s.File.Name = config.FileName || s.File.Name;
|
||||
configs.push(s);
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Configs = configs;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareSecretConfig(config, input) {
|
||||
if (input.Secrets) {
|
||||
var secrets = [];
|
||||
angular.forEach(input.Secrets, function(secret) {
|
||||
if (secret.model) {
|
||||
var s = SecretHelper.secretConfig(secret.model);
|
||||
s.File.Name = s.SecretName;
|
||||
if (secret.overrideTarget && secret.target && secret.target !== '') {
|
||||
s.File.Name = secret.target;
|
||||
}
|
||||
secrets.push(s);
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Secrets = secrets;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareResourcesCpuConfig(config, input) {
|
||||
// CPU Limit
|
||||
if (input.CpuLimit > 0) {
|
||||
config.TaskTemplate.Resources.Limits.NanoCPUs = input.CpuLimit * 1000000000;
|
||||
}
|
||||
// CPU Reservation
|
||||
if (input.CpuReservation > 0) {
|
||||
config.TaskTemplate.Resources.Reservations.NanoCPUs = input.CpuReservation * 1000000000;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareResourcesMemoryConfig(config, input) {
|
||||
// Memory Limit - Round to 0.125
|
||||
var memoryLimit = (Math.round(input.MemoryLimit * 8) / 8).toFixed(3);
|
||||
memoryLimit *= 1024 * 1024;
|
||||
if (input.MemoryLimitUnit === 'GB') {
|
||||
memoryLimit *= 1024;
|
||||
}
|
||||
if (memoryLimit > 0) {
|
||||
config.TaskTemplate.Resources.Limits.MemoryBytes = memoryLimit;
|
||||
}
|
||||
// Memory Resevation - Round to 0.125
|
||||
var memoryReservation = (Math.round(input.MemoryReservation * 8) / 8).toFixed(3);
|
||||
memoryReservation *= 1024 * 1024;
|
||||
if (input.MemoryReservationUnit === 'GB') {
|
||||
memoryReservation *= 1024;
|
||||
}
|
||||
if (memoryReservation > 0) {
|
||||
config.TaskTemplate.Resources.Reservations.MemoryBytes = memoryReservation;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareLogDriverConfig(config, input) {
|
||||
var logOpts = {};
|
||||
if (input.LogDriverName) {
|
||||
config.TaskTemplate.LogDriver = { Name: input.LogDriverName };
|
||||
if (input.LogDriverName !== 'none') {
|
||||
input.LogDriverOpts.forEach(function (opt) {
|
||||
if (opt.name) {
|
||||
logOpts[opt.name] = opt.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) {
|
||||
config.TaskTemplate.LogDriver.Options = logOpts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var input = $scope.formValues;
|
||||
var config = {
|
||||
Name: input.Name,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Mounts: []
|
||||
},
|
||||
Placement: {},
|
||||
Resources: {
|
||||
Limits: {},
|
||||
Reservations: {}
|
||||
}
|
||||
},
|
||||
Mode: {},
|
||||
EndpointSpec: {}
|
||||
};
|
||||
prepareSchedulingConfig(config, input);
|
||||
prepareImageConfig(config, input);
|
||||
preparePortsConfig(config, input);
|
||||
prepareCommandConfig(config, input);
|
||||
prepareEnvConfig(config, input);
|
||||
prepareLabelsConfig(config, input);
|
||||
prepareVolumes(config, input);
|
||||
prepareNetworks(config, input);
|
||||
prepareHostsEntries(config, input);
|
||||
prepareUpdateConfig(config, input);
|
||||
prepareConfigConfig(config, input);
|
||||
prepareSecretConfig(config, input);
|
||||
preparePlacementConfig(config, input);
|
||||
prepareResourcesCpuConfig(config, input);
|
||||
prepareResourcesMemoryConfig(config, input);
|
||||
prepareRestartPolicy(config, input);
|
||||
prepareLogDriverConfig(config, input);
|
||||
return config;
|
||||
}
|
||||
|
||||
function createNewService(config, accessControlData) {
|
||||
|
||||
var registry = $scope.formValues.Registry;
|
||||
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
|
||||
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
|
||||
Service.create(config).$promise
|
||||
.then(function success(data) {
|
||||
var serviceIdentifier = data.ID;
|
||||
var userId = Authentication.getUserDetails().ID;
|
||||
return ResourceControlService.applyResourceControl('service', serviceIdentifier, userId, accessControlData, []);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Service successfully created');
|
||||
$state.go('docker.services', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create service');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
$scope.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.create = function createService() {
|
||||
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1;
|
||||
|
||||
if (!validateForm(accessControlData, isAdmin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
var config = prepareConfiguration();
|
||||
createNewService(config, accessControlData);
|
||||
};
|
||||
|
||||
function initSlidersMaxValuesBasedOnNodeData(nodes) {
|
||||
var maxCpus = 0;
|
||||
var maxMemory = 0;
|
||||
for (var n in nodes) {
|
||||
if (nodes[n].CPUs && nodes[n].CPUs > maxCpus) {
|
||||
maxCpus = nodes[n].CPUs;
|
||||
}
|
||||
if (nodes[n].Memory && nodes[n].Memory > maxMemory) {
|
||||
maxMemory = nodes[n].Memory;
|
||||
}
|
||||
}
|
||||
if (maxCpus > 0) {
|
||||
$scope.state.sliderMaxCpu = maxCpus / 1000000000;
|
||||
} else {
|
||||
$scope.state.sliderMaxCpu = 32;
|
||||
}
|
||||
if (maxMemory > 0) {
|
||||
$scope.state.sliderMaxMemory = Math.floor(maxMemory / 1000 / 1000);
|
||||
} else {
|
||||
$scope.state.sliderMaxMemory = 32768;
|
||||
}
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
|
||||
$q.all({
|
||||
volumes: VolumeService.volumes(),
|
||||
networks: NetworkService.networks(true, true, false, false),
|
||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||
configs: apiVersion >= 1.30 ? ConfigService.configs() : [],
|
||||
nodes: NodeService.nodes(),
|
||||
settings: SettingsService.publicSettings(),
|
||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25)
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.availableVolumes = data.volumes;
|
||||
$scope.availableNetworks = data.networks;
|
||||
$scope.availableSecrets = data.secrets;
|
||||
$scope.availableConfigs = data.configs;
|
||||
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
||||
initSlidersMaxValuesBasedOnNodeData(data.nodes);
|
||||
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
$scope.isAdmin = userDetails.role === 1;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to initialize view');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
469
app/docker/views/services/create/createservice.html
Normal file
469
app/docker/views/services/create/createservice.html
Normal file
|
@ -0,0 +1,469 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Create service"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.services">Services</a> > Add service
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" autocomplete="off">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="service_name" placeholder="e.g. myService">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Image configuration
|
||||
</div>
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="formValues.Image" registry="formValues.Registry" auto-complete="true"></por-image-registry>
|
||||
</div>
|
||||
<!-- !image-and-registry -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Scheduling
|
||||
</div>
|
||||
<!-- scheduling-mode -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Scheduling mode
|
||||
</label>
|
||||
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
|
||||
<label class="btn btn-primary" ng-model="formValues.Mode" uib-btn-radio="'global'">Global</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.Mode" uib-btn-radio="'replicated'">Replicated</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-inline" ng-if="formValues.Mode === 'replicated'">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Replicas
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3" style="margin-left: 20px;">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !scheduling-mode -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ports configuration
|
||||
</div>
|
||||
<!-- port-mapping -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Port mapping</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="portBinding in formValues.Ports" style="margin-top: 2px;">
|
||||
<!-- host-port -->
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
|
||||
</div>
|
||||
<!-- !host-port -->
|
||||
<span style="margin: 0 10px 0 10px;">
|
||||
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
<!-- container-port -->
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<!-- !container-port -->
|
||||
<!-- protocol-actions -->
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="portBinding.Protocol" uib-btn-radio="'tcp'">TCP</label>
|
||||
<label class="btn btn-primary" ng-model="portBinding.Protocol" uib-btn-radio="'udp'">UDP</label>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="portBinding.PublishMode" uib-btn-radio="'ingress'">Ingress</label>
|
||||
<label class="btn btn-primary" ng-model="portBinding.PublishMode" uib-btn-radio="'host'">Host</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- !protocol-actions -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Image" ng-click="create()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Create the service</span>
|
||||
<span ng-show="state.actionInProgress">Creating service...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-pills nav-justified">
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command & Logging</a></li>
|
||||
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config & Restart</a></li>
|
||||
<li class="interactive" ng-if="applicationState.endpoint.apiVersion >= 1.25"><a data-target="#secrets" data-toggle="tab">Secrets</a></li>
|
||||
<li class="interactive"><a data-target="#configs" data-toggle="tab" ng-if="applicationState.endpoint.apiVersion >= 1.30">Configs</a></li>
|
||||
<li class="interactive"><a data-target="#resources-placement" data-toggle="tab" ng-click="refreshSlider()">Resources & Placement</a></li>
|
||||
</ul>
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
<!-- tab-command -->
|
||||
<div class="tab-pane active" id="command">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Command
|
||||
</div>
|
||||
<!-- command-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="formValues.Command" id="service_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-input -->
|
||||
<!-- entrypoint-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_entrypoint" class="col-sm-2 col-lg-1 control-label text-left">Entrypoint</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="formValues.EntryPoint" id="service_entrypoint" placeholder="e.g. /bin/sh -c">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !entrypoint-input -->
|
||||
<!-- workdir-user-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_workingdir" class="col-sm-2 col-lg-1 control-label text-left">Working Dir</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.WorkingDir" id="service_workingdir" placeholder="e.g. /myapp">
|
||||
</div>
|
||||
<label for="service_user" class="col-sm-1 control-label text-left">User</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.User" id="service_user" placeholder="e.g. nginx">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !workdir-user-input -->
|
||||
<!-- environment-variables -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Environment variables</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addEnvironmentVariable()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
|
||||
</span>
|
||||
</div>
|
||||
<!-- environment-variable-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="variable in formValues.Env" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !environment-variable-input-list -->
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Logging
|
||||
</div>
|
||||
<!-- logging-driver -->
|
||||
<div class="form-group">
|
||||
<label for="log-driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="form-control" ng-model="formValues.LogDriverName" id="log-driver">
|
||||
<option selected value="">Default logging driver</option>
|
||||
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Logging driver for service that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers can be found <a href="https://docs.docker.com/engine/admin/logging/overview/#supported-logging-drivers" target="_blank">in the Docker documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !logging-driver -->
|
||||
<!-- logging-opts -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">
|
||||
Options
|
||||
<portainer-tooltip position="top" message="Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation."></portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="!formValues.LogDriverName || formValues.LogDriverName === 'none' || addLogDriverOpt(formValues.LogDriverName)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add logging driver option
|
||||
</span>
|
||||
</div>
|
||||
<!-- logging-opts-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="opt in formValues.LogDriverOpts" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">option</span>
|
||||
<input type="text" class="form-control" ng-model="opt.name" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="opt.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLogDriverOpt($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- logging-opts-input-list -->
|
||||
</div>
|
||||
<!-- !logging-opts -->
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
<!-- tab-volume -->
|
||||
<div class="tab-pane" id="volumes">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- volumes -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Volume mapping</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addVolume()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
|
||||
</span>
|
||||
</div>
|
||||
<!-- volumes-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="volume in formValues.Volumes">
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<!-- volume-line1 -->
|
||||
<div class="col-sm-12 form-inline">
|
||||
<!-- container-path -->
|
||||
<div class="input-group input-group-sm col-sm-6">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container">
|
||||
</div>
|
||||
<!-- !container-path -->
|
||||
<!-- volume-type -->
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||
<div class="btn-group btn-group-sm" ng-if="isAdmin || allowBindMounts">
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- !volume-type -->
|
||||
</div>
|
||||
<!-- !volume-line1 -->
|
||||
<!-- volume-line2 -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
|
||||
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
|
||||
<!-- volume -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select class="form-control" ng-model="volume.Source" ng-options="vol.Id|truncate:30 for vol in availableVolumes">
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- !volume -->
|
||||
<!-- bind -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
|
||||
</div>
|
||||
<!-- !bind -->
|
||||
<!-- read-only -->
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !read-only -->
|
||||
</div>
|
||||
<!-- !volume-line2 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !volumes-input-list -->
|
||||
</div>
|
||||
<!-- !volumes -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-volume -->
|
||||
<!-- tab-network -->
|
||||
<div class="tab-pane" id="network">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- network-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_network" class="col-sm-2 col-lg-1 control-label text-left">Network</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" ng-model="formValues.Network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
</div>
|
||||
<!-- !network-input -->
|
||||
<!-- extra-networks -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Extra networks</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addExtraNetwork()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add extra network
|
||||
</span>
|
||||
</div>
|
||||
<!-- network-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 2px;">
|
||||
<select class="form-control" ng-model="network.Name">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeExtraNetwork($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !network-input-list -->
|
||||
</div>
|
||||
<!-- !extra-networks -->
|
||||
<!-- extra-hosts-variables -->
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Hosts file entries</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addHostsEntry()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add additional entry
|
||||
</span>
|
||||
</div>
|
||||
<!-- hosts-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="variable in formValues.HostsEntries" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. host:IP">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeHostsEntry($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !hosts-input-list -->
|
||||
</div>
|
||||
<!-- !extra-hosts-variables -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-network -->
|
||||
<!-- tab-labels -->
|
||||
<div class="tab-pane" id="labels">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Service labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add service label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
<!-- container-labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Container labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addContainerLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add container label
|
||||
</span>
|
||||
</div>
|
||||
<!-- container-labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.ContainerLabels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeContainerLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !container-labels-input-list -->
|
||||
</div>
|
||||
<!-- !container-labels-->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-labels -->
|
||||
<!-- tab-update-config -->
|
||||
<div class="tab-pane" id="update-config" ng-include="'app/docker/views/services/create/includes/update-restart.html'"></div>
|
||||
<!-- !tab-update-config -->
|
||||
<!-- tab-secrets -->
|
||||
<div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/docker/views/services/create/includes/secret.html'"></div>
|
||||
<!-- !tab-secrets -->
|
||||
<!-- tab-configs -->
|
||||
<div class="tab-pane" id="configs" ng-if="applicationState.endpoint.apiVersion >= 1.30" ng-include="'app/docker/views/services/create/includes/config.html'"></div>
|
||||
<!-- !tab-configs -->
|
||||
<!-- tab-resources-placement -->
|
||||
<div class="tab-pane" id="resources-placement" ng-include="'app/docker/views/services/create/includes/resources-placement.html'"></div>
|
||||
<!-- !tab-resources-placement -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
27
app/docker/views/services/create/includes/config.html
Normal file
27
app/docker/views/services/create/includes/config.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Configs</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addConfig()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add a config
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="config in formValues.Configs" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">config</span>
|
||||
<select class="form-control" ng-model="config.model" ng-options="config.Name for config in availableConfigs">
|
||||
<option value="" selected="selected">Select a config</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">Path in container</span>
|
||||
<input class="form-control" ng-model="config.FileName" placeholder="e.g. /path/in/container" />
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeConfig($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,130 @@
|
|||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resources
|
||||
</div>
|
||||
<!-- memory-reservation-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
Memory reservation
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<slider model="formValues.MemoryReservation" floor="0" ceil="state.sliderMaxMemory" step="256" ng-if="state.sliderMaxMemory"></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" min="0" class="form-control" ng-model="formValues.MemoryReservation">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Minimum memory available on a node to run a task (<b>MB</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-reservation-input -->
|
||||
<!-- memory-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
Memory limit
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<slider model="formValues.MemoryLimit" floor="0" ceil="state.sliderMaxMemory" step="256" ng-if="state.sliderMaxMemory"></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" min="0" class="form-control" ng-model="formValues.MemoryLimit">
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 7px;">
|
||||
<p class="small text-muted">
|
||||
Maximum memory usage per task (<b>MB</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-limit-input -->
|
||||
<!-- cpu-reservation-input -->
|
||||
<div class="form-group">
|
||||
<label for="cpu-reservation" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
CPU reservation
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<slider model="formValues.CpuReservation" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="state.sliderMaxCpu"></slider>
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 20px;">
|
||||
<p class="small text-muted">
|
||||
Minimum CPU available on a node to run a task
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-reservation-input -->
|
||||
<!-- cpu-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
CPU limit
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<slider model="formValues.CpuLimit" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="state.sliderMaxCpu"></slider>
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 20px;">
|
||||
<p class="small text-muted">
|
||||
Maximum CPU usage per task
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-limit-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Placement
|
||||
</div>
|
||||
<!-- placement-constraints -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Placement constraints</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="constraint in formValues.PlacementConstraints" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select name="constraintOperator" class="form-control" ng-model="constraint.operator">
|
||||
<option value="==">==</option>
|
||||
<option value="!=">!=</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !placement-constraints -->
|
||||
<!-- placement-preferences -->
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.30">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Placement preferences</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementPreference()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement preference
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="preference in formValues.PlacementPreferences" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">strategy</span>
|
||||
<input type="text" class="form-control" ng-model="preference.strategy" placeholder="e.g. spread">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="preference.value" placeholder="e.g. node.labels.datacenter">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementPreference($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !placement-preferences -->
|
||||
</form>
|
38
app/docker/views/services/create/includes/secret.html
Normal file
38
app/docker/views/services/create/includes/secret.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Secrets</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSecret()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add a secret
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="secret in formValues.Secrets track by $index" style="margin-top: 4px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">secret</span>
|
||||
<select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets">
|
||||
<option value="" selected="selected">Select a secret</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group col-sm-4 input-group-sm" ng-if="applicationState.endpoint.apiVersion >= 1.30 && secret.overrideTarget">
|
||||
<span class="input-group-addon">target</span>
|
||||
<input class="form-control" ng-model="secret.target" placeholder="/path/in/container">
|
||||
</div>
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<div class="btn-group btn-group-sm" ng-if="applicationState.endpoint.apiVersion >= 1.30">
|
||||
<label class="btn btn-primary" ng-model="secret.overrideTarget" uib-btn-radio="false">Default location</label>
|
||||
<label class="btn btn-primary" ng-model="secret.overrideTarget" uib-btn-radio="true">Override</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSecret($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
132
app/docker/views/services/create/includes/update-restart.html
Normal file
132
app/docker/views/services/create/includes/update-restart.html
Normal file
|
@ -0,0 +1,132 @@
|
|||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Update config
|
||||
</div>
|
||||
<!-- parallelism-input -->
|
||||
<div class="form-group">
|
||||
<label for="parallelism" class="col-sm-3 col-lg-2 control-label text-left">Update parallelism</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<input type="number" class="form-control" ng-model="formValues.Parallelism" id="parallelism" placeholder="e.g. 1">
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Maximum number of tasks to be updated simultaneously (0 to update all at once).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !parallelism-input -->
|
||||
<!-- delay-input -->
|
||||
<div class="form-group">
|
||||
<label for="update-delay" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Update delay
|
||||
<portainer-tooltip position="bottom" message="Supported format examples: 1h, 5m, 10s, 1000ms, 15us, 60ns."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.UpdateDelay" id="update-delay" placeholder="e.g. 1m" ng-pattern="/^([0-9]+)(h|m|s|ms|us|ns)$/i">
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Amount of time between updates expressed by a number followed by unit (ns|us|ms|s|m|h). Default value is 0s, 0 seconds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !delay-input -->
|
||||
<!-- failureAction-input -->
|
||||
<div class="form-group">
|
||||
<label for="failure-action" class="col-sm-3 col-lg-2 control-label text-left">Update failure action</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'continue'">Continue</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'pause'">Pause</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Action taken on failure to start after update.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !failureAction-input -->
|
||||
<!-- order-input -->
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.29">
|
||||
<label for="update-order" class="col-sm-3 col-lg-2 control-label text-left">Update order</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="formValues.UpdateOrder" uib-btn-radio="'start-first'">start-first</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.UpdateOrder" uib-btn-radio="'stop-first'">stop-first</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Operation order on failure.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !order-input -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Restart policy
|
||||
</div>
|
||||
<!-- restartCondition-input -->
|
||||
<div class="form-group">
|
||||
<label for="restart-condition" class="col-sm-3 col-lg-2 control-label text-left">Restart condition</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="formValues.RestartCondition" uib-btn-radio="'none'">None</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.RestartCondition" uib-btn-radio="'on-failure'">On-failure</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.RestartCondition" uib-btn-radio="'any'">Any</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Restart when condition is met (default condition "any").
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restartCondition-input -->
|
||||
<!-- restartDelay-input -->
|
||||
<div class="form-group">
|
||||
<label for="restart-delay" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Restart delay
|
||||
<portainer-tooltip position="bottom" message="Supported format examples: 1h, 5m, 10s, 1000ms, 15us, 60ns."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.RestartDelay" id="restart-delay" placeholder="e.g. 1m" ng-pattern="/^([0-9]+)(h|m|s|ms|us|ns)$/i">
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Delay between restart attempts expressed by a number followed by unit (ns|us|ms|s|m|h). Default value is 5s, 5 seconds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restartDelay-input -->
|
||||
<!-- restartMaxAttempts-input -->
|
||||
<div class="form-group">
|
||||
<label for="restart-max-attempts" class="col-sm-3 col-lg-2 control-label text-left">Restart max attempts</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<input type="number" class="form-control" ng-model="formValues.RestartMaxAttempts" id="restart-max-attempts" placeholder="e.g. 0">
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Maximum attempts to restart a given task before giving up (default value is 0, which means unlimited).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restartMaxAttempts-input -->
|
||||
<!-- restartWindow-input -->
|
||||
<div class="form-group">
|
||||
<label for="restart-window" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Restart window
|
||||
<portainer-tooltip position="bottom" message="Supported format examples: 1h, 5m, 10s, 1000ms, 15us, 60ns."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.RestartWindow" id="restart-window" placeholder="e.g. 1m" ng-pattern="/^([0-9]+)(h|m|s|ms|us|ns)$/i">
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Time window to evaluate restart attempts expressed by a number followed by unit (ns|us|ms|s|m|h). Default value is 0 seconds, which is unbounded.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restartWindow-input -->
|
||||
</form>
|
62
app/docker/views/services/edit/includes/configs.html
Normal file
62
app/docker/views/services/edit/includes/configs.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<form ng-if="applicationState.endpoint.apiVersion >= 1.30" id="service-configs" ng-submit="updateService(service)">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Configs">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px;">
|
||||
Add a config:
|
||||
<select class="form-control" ng-options="config.Name for config in configs" ng-model="newConfig">
|
||||
<option selected disabled hidden value="">Select a config</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add config
|
||||
</a>
|
||||
</div>
|
||||
<table class="table" style="margin-top: 5px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Path in container</th>
|
||||
<th>UID</th>
|
||||
<th>GID</th>
|
||||
<th>Mode</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="config in service.ServiceConfigs">
|
||||
<td><a ui-sref="docker.configs.config({id: config.Id})">{{ config.Name }}</a></td>
|
||||
<td>
|
||||
<input class="form-control" ng-model="config.FileName" ng-change="updateConfig(service)" placeholder="e.g. /path/in/container" required />
|
||||
</td>
|
||||
<td>{{ config.Uid }}</td>
|
||||
<td>{{ config.Gid }}</td>
|
||||
<td>{{ config.Mode }}</td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-danger pull-right" type="button" ng-click="removeConfig(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i> Remove config
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.ServiceConfigs.length === 0">
|
||||
<td colspan="6" class="text-center text-muted">No configs associated to this service.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceConfigs'])">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServiceConfigs'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</form>
|
66
app/docker/views/services/edit/includes/constraints.html
Normal file
66
app/docker/views/services/edit/includes/constraints.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
<div ng-if="service.ServiceConstraints" id="service-placement-constraints">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Placement constraints">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addPlacementConstraint(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="service.ServiceConstraints.length === 0">
|
||||
<p>There are no placement constraints for this service.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.ServiceConstraints.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Operator</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="constraint in service.ServiceConstraints">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="constraintOperator" class="form-control" ng-model="constraint.operator" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
|
||||
<option value="==">==</option>
|
||||
<option value="!=">!=</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceConstraints'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServiceConstraints'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
56
app/docker/views/services/edit/includes/container-specs.html
Normal file
56
app/docker/views/services/edit/includes/container-specs.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
<div>
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Container spec"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CMD</td>
|
||||
<td><code ng-if="service.Command">{{ service.Command|command }}</code></td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Command to execute.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Args</td>
|
||||
<td><code ng-if="service.Arguments">{{ service.Arguments }}</code></td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Arguments passed to command in container.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>User</td>
|
||||
<td>{{ service.User }}</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Username or UID.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Working directory</td>
|
||||
<td>{{ service.Dir }}</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Working directory inside the container.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stop grace period</td>
|
||||
<td>{{ service.StopGracePeriod }}</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Time to wait before force killing a container (default none).
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
59
app/docker/views/services/edit/includes/containerlabels.html
Normal file
59
app/docker/views/services/edit/includes/containerlabels.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
<div id="service-container-labels">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Container labels">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addContainerLabel(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="service.ServiceContainerLabels.length === 0">
|
||||
<p>There are no container labels for this service.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.ServiceContainerLabels.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="label in service.ServiceContainerLabels">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateContainerLabel(service, label)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateContainerLabel(service, label)" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeContainerLabel(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceContainerLabels'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServiceContainerLabels'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,59 @@
|
|||
<div ng-if="service.EnvironmentVariables" id="service-env-variables">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Environment variables">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addEnvironmentVariable(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="service.EnvironmentVariables.length === 0">
|
||||
<p>There are no environment variables for this service.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.EnvironmentVariables.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="var in service.EnvironmentVariables">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="var.key" ng-disabled="var.added || isUpdating" placeholder="e.g. FOO">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="var.value" ng-change="updateEnvironmentVariable(service, var)" placeholder="e.g. bar" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['EnvironmentVariables'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['EnvironmentVariables'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
57
app/docker/views/services/edit/includes/hosts.html
Normal file
57
app/docker/views/services/edit/includes/hosts.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<div>
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Hosts file entries">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addHostsEntry(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add host entry
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="!service.Hosts || service.Hosts.length === 0">
|
||||
<p>The Hosts file has no extra entries.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.Hosts.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="entry in service.Hosts">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="entry.hostname" placeholder="e.g. example.com" ng-change="updateHostsEntry(service, entry)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="entry.ip" placeholder="e.g. 10.0.1.1" ng-change="updateHostsEntry(service, entry)" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeHostsEntry(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['Hosts'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['Hosts'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
65
app/docker/views/services/edit/includes/logging.html
Normal file
65
app/docker/views/services/edit/includes/logging.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
<div id="service-logging-driver">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Logging driver">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px;">
|
||||
Driver:
|
||||
<select class="form-control" ng-model="service.LogDriverName" ng-change="updateLogDriverName(service)" ng-disabled="isUpdating">
|
||||
<option selected value="">Default logging driver</option>
|
||||
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="!service.LogDriverName || service.LogDriverName === 'none' || addLogDriverOpt(service)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add logging driver option
|
||||
</a>
|
||||
</div>
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="option in service.LogDriverOpts">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="option.key" ng-disabled="option.added || isUpdating" placeholder="e.g. FOO">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="option.value" ng-change="updateLogDriverOpt(service, option)" placeholder="e.g. bar" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLogDriverOpt(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.LogDriverOpts.length === 0">
|
||||
<td colspan="6" class="text-center text-muted">No options associated to this logging driver.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['LogDriverName', 'LogDriverOpts'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['LogDriverName', 'LogDriverOpts'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
67
app/docker/views/services/edit/includes/mounts.html
Normal file
67
app/docker/views/services/edit/includes/mounts.html
Normal file
|
@ -0,0 +1,67 @@
|
|||
<div ng-if="service.ServiceMounts" id="service-mounts">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Mounts">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addMount(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> mount
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="service.ServiceMounts.length === 0">
|
||||
<p>There are no mounts for this service.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.ServiceMounts.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Source</th>
|
||||
<th>Target</th>
|
||||
<th>Read only</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mount in service.ServiceMounts">
|
||||
<td>
|
||||
<select name="mountType" class="form-control" ng-model="mount.Type" ng-disabled="isUpdating">
|
||||
<option value="volume">Volume</option>
|
||||
<option value="bind">Bind</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" ng-model="mount.Source" placeholder="e.g. /tmp/portainer/data" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" ng-model="mount.Target" placeholder="e.g. /tmp/portainer/data" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
|
||||
</td>
|
||||
<td>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeMount(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServiceMounts'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
26
app/docker/views/services/edit/includes/networks.html
Normal file
26
app/docker/views/services/edit/includes/networks.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<div id="service-network-specs">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Networks"></rd-widget-header>
|
||||
<rd-widget-body ng-if="!service.VirtualIPs || service.VirtualIPs.length === 0">
|
||||
<p>This service is not connected to any networks.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.VirtualIPs && service.VirtualIPs.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>IP address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="network in service.VirtualIPs">
|
||||
<td>
|
||||
<a ui-sref="docker.networks.network({id: network.NetworkID})">{{ network.NetworkID }}</a>
|
||||
</td>
|
||||
<td>{{ network.Addr }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,57 @@
|
|||
<div ng-if="service.ServicePreferences" id="service-placement-preferences">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Placement preferences">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addPlacementPreference(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement preference
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="service.ServicePreferences.length === 0">
|
||||
<p>There are no placement preferences for this service.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.ServicePreferences.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Strategy</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="preference in service.ServicePreferences">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="preference.strategy" placeholder="e.g. node.role" ng-change="updatePlacementPreference(service, preference)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="preference.value" placeholder="e.g. manager" ng-change="updatePlacementPreference(service, preference)" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementPreference(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServicePreferences'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServicePreferences'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
80
app/docker/views/services/edit/includes/ports.html
Normal file
80
app/docker/views/services/edit/includes/ports.html
Normal file
|
@ -0,0 +1,80 @@
|
|||
<div>
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Published ports">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addPublishedPort(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> port mapping
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="!service.Ports || service.Ports.length === 0">
|
||||
<p>This service has no ports published.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.Ports && service.Ports.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host port</th>
|
||||
<th>Container port</th>
|
||||
<th>Protocol</th>
|
||||
<th>Publish mode</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="portBinding in service.Ports">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="number" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 8080" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="number" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<select class="selectpicker form-control" ng-model="portBinding.Protocol" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<select class="selectpicker form-control" ng-model="portBinding.PublishMode" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
|
||||
<option value="ingress">ingress</option>
|
||||
<option value="host">host</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortPublishedBinding(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['Ports'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['Ports'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
82
app/docker/views/services/edit/includes/resources.html
Normal file
82
app/docker/views/services/edit/includes/resources.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
<div id="service-resources">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Resource limits and reservations">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align : middle;">
|
||||
Memory reservation (MB)
|
||||
</td>
|
||||
<td>
|
||||
<input class="input-sm" type="number" step="0.125" min="0" ng-model="service.ReservationMemoryBytes" ng-change="updateServiceAttribute(service, 'ReservationMemoryBytes')" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td style="vertical-align : middle;">
|
||||
<p class="small text-muted">
|
||||
Minimum memory available on a node to run a task (set to 0 for unlimited)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align : middle;">
|
||||
Memory limit (MB)
|
||||
</td>
|
||||
<td>
|
||||
<input class="input-sm" type="number" step="0.125" min="0" ng-model="service.LimitMemoryBytes" ng-change="updateServiceAttribute(service, 'LimitMemoryBytes')" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td style="vertical-align : middle;">
|
||||
<p class="small text-muted">
|
||||
Maximum memory usage per task (set to 0 for unlimited)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align : middle;">
|
||||
<div>
|
||||
CPU reservation
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<slider model="service.ReservationNanoCPUs" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="service && state.sliderMaxCpu" on-change="updateServiceAttribute(service, 'ReservationNanoCPUs')"></slider>
|
||||
</td>
|
||||
<td style="vertical-align : middle;">
|
||||
<p class="small text-muted">
|
||||
Minimum CPU available on a node to run a task
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align : middle;">
|
||||
<div>
|
||||
CPU limit
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<slider model="service.LimitNanoCPUs" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="service && state.sliderMaxCpu" on-change="updateServiceAttribute(service, 'LimitNanoCPUs')"></slider>
|
||||
</td>
|
||||
<td style="vertical-align : middle;">
|
||||
<p class="small text-muted">
|
||||
Maximum CPU usage per task
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['LimitNanoCPUs', 'LimitMemoryBytes', 'ReservationNanoCPUs', 'ReservationMemoryBytes'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['LimitNanoCPUs', 'LimitMemoryBytes', 'ReservationNanoCPUs', 'ReservationMemoryBytes'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
76
app/docker/views/services/edit/includes/restart.html
Normal file
76
app/docker/views/services/edit/includes/restart.html
Normal file
|
@ -0,0 +1,76 @@
|
|||
<div id="service-restart-policy">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Restart policy">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Restart condition</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<select class="selectpicker form-control" ng-model="service.RestartCondition" ng-change="updateServiceAttribute(service, 'RestartCondition')" ng-disabled="isUpdating">
|
||||
<option value="none">None</option>
|
||||
<option value="on-failure">On failure</option>
|
||||
<option value="any">Any</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Condition for restart.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Restart delay</td>
|
||||
<td>
|
||||
<input class="input-sm" type="text" ng-model="service.RestartDelay" ng-change="updateServiceAttribute(service, 'RestartDelay')" ng-pattern="/^([0-9]+)(h|m|s|ms|us|ns)$/i" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Delay between restart attempts expressed by a number followed by unit (ns|us|ms|s|m|h). Default value is 5s, 5 seconds.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Restart max attempts</td>
|
||||
<td>
|
||||
<input class="input-sm" type="number" ng-model="service.RestartMaxAttempts" ng-change="updateServiceAttribute(service, 'RestartMaxAttempts')" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Maximum attempts to restart a given task before giving up (default value is 0, which means unlimited).
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Restart window</td>
|
||||
<td>
|
||||
<input class="input-sm" type="text" ng-model="service.RestartWindow" ng-change="updateServiceAttribute(service, 'RestartWindow')" ng-pattern="/^([0-9]+)(h|m|s|ms|us|ns)$/i" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Time window to evaluate restart attempts expressed by a number followed by unit (ns|us|ms|s|m|h). Default value is 0 seconds, which is unbounded.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['RestartCondition', 'RestartDelay', 'RestartMaxAttempts', 'RestartWindow'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['RestartCondition', 'RestartDelay', 'RestartMaxAttempts', 'RestartWindow'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
68
app/docker/views/services/edit/includes/secrets.html
Normal file
68
app/docker/views/services/edit/includes/secrets.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
<div ng-if="applicationState.endpoint.apiVersion >= 1.25" id="service-secrets">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Secrets">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px;">
|
||||
Add a secret:
|
||||
<select class="form-control" ng-options="secret.Name for secret in secrets" ng-model="state.addSecret.secret">
|
||||
<option selected disabled hidden value="">Select a secret</option>
|
||||
</select>
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.30 && state.addSecret.override">
|
||||
Target:
|
||||
<input class="form-control" ng-model="state.addSecret.target" placeholder="/path/in/container">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" ng-if="applicationState.endpoint.apiVersion >= 1.30">
|
||||
<label class="btn btn-primary" ng-model="state.addSecret.override" uib-btn-radio="false">Default location</label>
|
||||
<label class="btn btn-primary" ng-model="state.addSecret.override" uib-btn-radio="true">Override</label>
|
||||
</div>
|
||||
<a class="btn btn-default btn-sm" ng-click="addSecret(service, state.addSecret)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add secret
|
||||
</a>
|
||||
</div>
|
||||
<table class="table" style="margin-top: 5px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>File name</th>
|
||||
<th>UID</th>
|
||||
<th>GID</th>
|
||||
<th>Mode</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="secret in service.ServiceSecrets">
|
||||
<td><a ui-sref="docker.secrets.secret({id: secret.Id})">{{ secret.Name }}</a></td>
|
||||
<td>{{ secret.FileName }}</td>
|
||||
<td>{{ secret.Uid }}</td>
|
||||
<td>{{ secret.Gid }}</td>
|
||||
<td>{{ secret.Mode }}</td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-danger pull-right" type="button" ng-click="removeSecret(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i> Remove secret
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.ServiceSecrets.length === 0">
|
||||
<td colspan="6" class="text-center text-muted">No secrets associated to this service.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceSecrets'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServiceSecrets'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
63
app/docker/views/services/edit/includes/servicelabels.html
Normal file
63
app/docker/views/services/edit/includes/servicelabels.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
<div id="service-labels">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Service labels">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addLabel(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="service.ServiceLabels.length === 0">
|
||||
<p>There are no labels for this service.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body classes="no-padding" ng-if="service.ServiceLabels.length > 0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Label
|
||||
</th>
|
||||
<th>
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="label in service.ServiceLabels">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceLabels'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServiceLabels'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
10
app/docker/views/services/edit/includes/tasks.html
Normal file
10
app/docker/views/services/edit/includes/tasks.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div ng-if="tasks.length > 0 && nodes" id="service-tasks">
|
||||
<tasks-datatable
|
||||
title="Tasks" title-icon="fa-tasks"
|
||||
dataset="tasks" table-key="service-tasks"
|
||||
order-by="Updated" reverse-order="true"
|
||||
nodes="nodes"
|
||||
show-text-filter="true"
|
||||
show-slot-column="service.Mode !== 'global'"
|
||||
></tasks-datatable>
|
||||
</div>
|
88
app/docker/views/services/edit/includes/updateconfig.html
Normal file
88
app/docker/views/services/edit/includes/updateconfig.html
Normal file
|
@ -0,0 +1,88 @@
|
|||
<div id="service-update-config">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Update configuration">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Update Parallelism</td>
|
||||
<td>
|
||||
<input class="input-sm" type="number" ng-model="service.UpdateParallelism" ng-change="updateServiceAttribute(service, 'UpdateParallelism')" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Maximum number of tasks to be updated simultaneously (0 to update all at once).
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Update Delay</td>
|
||||
<td>
|
||||
<input class="input-sm" type="text" ng-model="service.UpdateDelay" ng-change="updateServiceAttribute(service, 'UpdateDelay')" ng-pattern="/^([0-9]+)(h|m|s|ms|us|ns)$/i" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Amount of time between updates expressed by a number followed by unit (ns|us|ms|s|m|h). Example: 1m.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Update Failure Action</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.UpdateFailureAction" value="continue" ng-change="updateServiceAttribute(service, 'UpdateFailureAction')" ng-disabled="isUpdating">
|
||||
Continue
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.UpdateFailureAction" value="pause" ng-change="updateServiceAttribute(service, 'UpdateFailureAction')" ng-disabled="isUpdating">
|
||||
Pause
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Action taken on failure to start after update.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="applicationState.endpoint.apiVersion >= 1.29">
|
||||
<td>Order</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="updateconfig_order" ng-model="service.UpdateOrder" value="start-first" ng-change="updateServiceAttribute(service, 'UpdateOrder')" ng-disabled="isUpdating">
|
||||
start-first
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="updateconfig_order" ng-model="service.UpdateOrder" value="stop-first" ng-change="updateServiceAttribute(service, 'UpdateOrder')" ng-disabled="isUpdating">
|
||||
stop-first
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Operation order on failure.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['UpdateFailureAction', 'UpdateDelay', 'UpdateParallelism', 'UpdateOrder'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['UpdateFailureAction', 'UpdateDelay', 'UpdateParallelism', 'UpdateOrder'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
180
app/docker/views/services/edit/service.html
Normal file
180
app/docker/views/services/edit/service.html
Normal file
|
@ -0,0 +1,180 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Service details">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker.services.service({id: service.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.Id})">{{ service.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div ng-if="isUpdating" class="col-lg-12 col-md-12 col-xs-12">
|
||||
<div class="alert alert-info" role="alert" id="service-update-alert">
|
||||
<p>This service is being updated. Editing this service is currently disabled.</p>
|
||||
<a ui-sref="docker.services.service({id: service.Id}, {reload: true})">Refresh to see if this service has finished updated.</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-9 col-md-9 col-xs-9">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Service details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td ng-if="applicationState.endpoint.apiVersion <= 1.24">
|
||||
<input type="text" class="form-control" ng-model="service.Name" ng-change="updateServiceAttribute(service, 'Name')" ng-disabled="isUpdating">
|
||||
</td>
|
||||
<td ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||
{{ service.Name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ service.Id }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.CreatedAt">
|
||||
<td>Created at</td>
|
||||
<td>{{ service.CreatedAt|getisodate}}</td>
|
||||
</tr>
|
||||
<tr ng-if="service.UpdatedAt">
|
||||
<td>Last updated at</td>
|
||||
<td>{{ service.UpdatedAt|getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Version">
|
||||
<td>Version</td>
|
||||
<td>{{ service.Version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scheduling mode</td>
|
||||
<td>{{ service.Mode }}</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Mode === 'replicated'">
|
||||
<td>Replicas</td>
|
||||
<td>
|
||||
<span ng-if="service.Mode === 'replicated'">
|
||||
<input class="input-sm" type="number" ng-model="service.Replicas" ng-change="updateServiceAttribute(service, 'Replicas')" ng-disabled="isUpdating" />
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" uib-typeahead="image for image in availableImages | filter:$viewValue | limitTo:5"
|
||||
ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" ng-disabled="isUpdating">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a ng-if="applicationState.endpoint.apiVersion >= 1.30" class="btn btn-primary btn-sm" type="button" ui-sref="docker.services.service.logs({id: service.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Service logs</a>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || isUpdating" ng-click="forceUpdateService(service)" button-spinner="state.updateInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||
<span ng-hide="state.updateInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Update the service</span>
|
||||
<span ng-show="state.updateInProgress">Update in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-disabled="state.deletionInProgress || isUpdating" ng-click="removeService()" button-spinner="state.deletionInProgress">
|
||||
<span ng-hide="state.deletionInProgress"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete the service</span>
|
||||
<span ng-show="state.deletionInProgress">Deletion in progress...</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<p class="small text-muted">
|
||||
Do you need help? View the Docker Service documentation <a href="https://docs.docker.com/engine/reference/commandline/service_update/" target="self">here</a>.
|
||||
</p>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(service, ['Mode', 'Replicas', 'Image', 'Name'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['Mode', 'Replicas', 'Image', 'Name'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-3 col-xs-3">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-bars" title="Quick navigation"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li><a href ng-click="goToItem('service-env-variables')">Environment variables</a></li>
|
||||
<li><a href ng-click="goToItem('service-container-labels')">Container labels</a></li>
|
||||
<li><a href ng-click="goToItem('service-mounts')">Mounts</a></li>
|
||||
<li><a href ng-click="goToItem('service-network-specs')">Network & published ports</a></li>
|
||||
<li><a href ng-click="goToItem('service-resources')">Resource limits & reservations</a></li>
|
||||
<li><a href ng-click="goToItem('service-placement-constraints')">Placement constraints</a></li>
|
||||
<li ng-if="applicationState.endpoint.apiVersion >= 1.30"><a href ng-click="goToItem('service-placement-preferences')">Placement preferences</a></li>
|
||||
<li><a href ng-click="goToItem('service-restart-policy')">Restart policy</a></li>
|
||||
<li><a href ng-click="goToItem('service-update-config')">Update configuration</a></li>
|
||||
<li><a href ng-click="goToItem('service-logging')">Logging</a></li>
|
||||
<li><a href ng-click="goToItem('service-labels')">Service labels</a></li>
|
||||
<li><a href ng-click="goToItem('service-configs')">Configs</a></li>
|
||||
<li ng-if="applicationState.endpoint.apiVersion >= 1.25"><a href ng-click="goToItem('service-secrets')">Secrets</a></li>
|
||||
<li><a href ng-click="goToItem('service-tasks')">Tasks</a></li>
|
||||
</ul>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- access-control-panel -->
|
||||
<por-access-control-panel
|
||||
ng-if="service && applicationState.application.authentication"
|
||||
resource-id="service.Id"
|
||||
resource-control="service.ResourceControl"
|
||||
resource-type="'service'">
|
||||
</por-access-control-panel>
|
||||
<!-- !access-control-panel -->
|
||||
|
||||
<div class="row">
|
||||
<hr>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<h3 id="container-specs">Container specification</h3>
|
||||
<div id="service-container-spec" class="padding-top" ng-include="'app/docker/views/services/edit/includes/container-specs.html'"></div>
|
||||
<div id="service-env-variables" class="padding-top" ng-include="'app/docker/views/services/edit/includes/environmentvariables.html'"></div>
|
||||
<div id="service-container-labels" class="padding-top" ng-include="'app/docker/views/services/edit/includes/containerlabels.html'"></div>
|
||||
<div id="service-mounts" class="padding-top" ng-include="'app/docker/views/services/edit/includes/mounts.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<hr>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<h3 id="service-network-specs">Networks & ports</h3>
|
||||
<div id="service-networks" class="padding-top" ng-include="'app/docker/views/services/edit/includes/networks.html'"></div>
|
||||
<div id="service-published-ports" class="padding-top" ng-include="'app/docker/views/services/edit/includes/ports.html'"></div>
|
||||
<div id="service-hosts-entries" class="padding-top" ng-include="'app/docker/views/services/edit/includes/hosts.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<hr>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<h3 id="service-specs">Service specification</h3>
|
||||
<div id="service-resources" class="padding-top" ng-include="'app/docker/views/services/edit/includes/resources.html'"></div>
|
||||
<div id="service-placement-constraints" class="padding-top" ng-include="'app/docker/views/services/edit/includes/constraints.html'"></div>
|
||||
<div id="service-placement-preferences" ng-if="applicationState.endpoint.apiVersion >= 1.30" class="padding-top" ng-include="'app/docker/views/services/edit/includes/placementPreferences.html'"></div>
|
||||
<div id="service-restart-policy" class="padding-top" ng-include="'app/docker/views/services/edit/includes/restart.html'"></div>
|
||||
<div id="service-update-config" class="padding-top" ng-include="'app/docker/views/services/edit/includes/updateconfig.html'"></div>
|
||||
<div id="service-logging" class="padding-top" ng-include="'app/docker/views/services/edit/includes/logging.html'"></div>
|
||||
<div id="service-labels" class="padding-top" ng-include="'app/docker/views/services/edit/includes/servicelabels.html'"></div>
|
||||
<div id="service-configs" class="padding-top" ng-include="'app/docker/views/services/edit/includes/configs.html'"></div>
|
||||
<div id="service-secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" class="padding-top" ng-include="'app/docker/views/services/edit/includes/secrets.html'"></div>
|
||||
<div id="service-tasks" class="padding-top" ng-include="'app/docker/views/services/edit/includes/tasks.html'"></div>
|
||||
</div>
|
||||
</div>
|
481
app/docker/views/services/edit/serviceController.js
Normal file
481
app/docker/views/services/edit/serviceController.js
Normal file
|
@ -0,0 +1,481 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'ModalService', 'PluginService',
|
||||
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, ModalService, PluginService) {
|
||||
|
||||
$scope.state = {
|
||||
updateInProgress: false,
|
||||
deletionInProgress: false
|
||||
};
|
||||
|
||||
$scope.tasks = [];
|
||||
$scope.availableImages = [];
|
||||
|
||||
$scope.lastVersion = 0;
|
||||
|
||||
var originalService = {};
|
||||
var previousServiceValues = [];
|
||||
|
||||
$scope.renameService = function renameService(service) {
|
||||
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
|
||||
service.EditName = false;
|
||||
};
|
||||
$scope.changeServiceImage = function changeServiceImage(service) {
|
||||
updateServiceAttribute(service, 'Image', service.newServiceImage || service.image);
|
||||
service.EditImage = false;
|
||||
};
|
||||
$scope.scaleService = function scaleService(service) {
|
||||
var replicas = service.newServiceReplicas === null || isNaN(service.newServiceReplicas) ? service.Replicas : service.newServiceReplicas;
|
||||
updateServiceAttribute(service, 'Replicas', replicas);
|
||||
service.EditReplicas = false;
|
||||
};
|
||||
|
||||
$scope.goToItem = function(hash) {
|
||||
if ($location.hash() !== hash) {
|
||||
$location.hash(hash);
|
||||
} else {
|
||||
$anchorScroll();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
|
||||
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
|
||||
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
|
||||
};
|
||||
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
|
||||
var removedElement = service.EnvironmentVariables.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
|
||||
}
|
||||
};
|
||||
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
|
||||
if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) {
|
||||
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, newSecret) {
|
||||
if (newSecret.secret) {
|
||||
var filename = newSecret.secret.Name;
|
||||
if (newSecret.override) {
|
||||
filename = newSecret.target;
|
||||
}
|
||||
if (service.ServiceSecrets.filter(function(serviceSecret) { return serviceSecret.Id === newSecret.secret.Id && serviceSecret.FileName === filename;}).length === 0) {
|
||||
service.ServiceSecrets.push({ Id: newSecret.secret.Id, Name: newSecret.secret.Name, FileName: filename, Uid: '0', Gid: '0', Mode: 444 });
|
||||
updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets);
|
||||
}
|
||||
}
|
||||
};
|
||||
$scope.removeSecret = function removeSecret(service, index) {
|
||||
var removedElement = service.ServiceSecrets.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets);
|
||||
}
|
||||
};
|
||||
$scope.addLabel = function addLabel(service) {
|
||||
service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
|
||||
updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
|
||||
};
|
||||
$scope.removeLabel = function removeLabel(service, index) {
|
||||
var removedElement = service.ServiceLabels.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
|
||||
}
|
||||
};
|
||||
$scope.updateLabel = function updateLabel(service, label) {
|
||||
if (label.value !== label.originalValue || label.key !== label.originalKey) {
|
||||
updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
|
||||
}
|
||||
};
|
||||
$scope.addContainerLabel = function addContainerLabel(service) {
|
||||
service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' });
|
||||
updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
|
||||
};
|
||||
$scope.removeContainerLabel = function removeLabel(service, index) {
|
||||
var removedElement = service.ServiceContainerLabels.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
|
||||
}
|
||||
};
|
||||
$scope.updateContainerLabel = function updateLabel(service, label) {
|
||||
if (label.value !== label.originalValue || label.key !== label.originalKey) {
|
||||
updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
|
||||
}
|
||||
};
|
||||
$scope.addMount = function addMount(service) {
|
||||
service.ServiceMounts.push({Type: 'volume', Source: '', Target: '', ReadOnly: false });
|
||||
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
|
||||
};
|
||||
$scope.removeMount = function removeMount(service, index) {
|
||||
var removedElement = service.ServiceMounts.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
|
||||
}
|
||||
};
|
||||
$scope.updateMount = function updateMount(service, mount) {
|
||||
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
|
||||
};
|
||||
$scope.addPlacementConstraint = function addPlacementConstraint(service) {
|
||||
service.ServiceConstraints.push({ key: '', operator: '==', value: '' });
|
||||
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
|
||||
};
|
||||
$scope.removePlacementConstraint = function removePlacementConstraint(service, index) {
|
||||
var removedElement = service.ServiceConstraints.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
|
||||
}
|
||||
};
|
||||
$scope.updatePlacementConstraint = function(service, constraint) {
|
||||
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
|
||||
};
|
||||
|
||||
$scope.addPlacementPreference = function(service) {
|
||||
service.ServicePreferences.push({ strategy: 'spread', value: '' });
|
||||
updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
|
||||
};
|
||||
$scope.removePlacementPreference = function(service, index) {
|
||||
var removedElement = service.ServicePreferences.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
|
||||
}
|
||||
};
|
||||
$scope.updatePlacementPreference = function(service, constraint) {
|
||||
updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
|
||||
};
|
||||
|
||||
$scope.addPublishedPort = function addPublishedPort(service) {
|
||||
if (!service.Ports) {
|
||||
service.Ports = [];
|
||||
}
|
||||
service.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
|
||||
};
|
||||
$scope.updatePublishedPort = function updatePublishedPort(service, portMapping) {
|
||||
updateServiceArray(service, 'Ports', service.Ports);
|
||||
};
|
||||
$scope.removePortPublishedBinding = function removePortPublishedBinding(service, index) {
|
||||
var removedElement = service.Ports.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'Ports', service.Ports);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addLogDriverOpt = function addLogDriverOpt(service) {
|
||||
service.LogDriverOpts.push({ key: '', value: '', originalValue: '' });
|
||||
updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts);
|
||||
};
|
||||
$scope.removeLogDriverOpt = function removeLogDriverOpt(service, index) {
|
||||
var removedElement = service.LogDriverOpts.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts);
|
||||
}
|
||||
};
|
||||
$scope.updateLogDriverOpt = function updateLogDriverOpt(service, variable) {
|
||||
if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) {
|
||||
updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts);
|
||||
}
|
||||
};
|
||||
$scope.updateLogDriverName = function updateLogDriverName(service) {
|
||||
updateServiceArray(service, 'LogDriverName', service.LogDriverName);
|
||||
};
|
||||
|
||||
$scope.addHostsEntry = function (service) {
|
||||
if (!service.Hosts) {
|
||||
service.Hosts = [];
|
||||
}
|
||||
service.Hosts.push({ hostname: '', ip: '' });
|
||||
};
|
||||
$scope.removeHostsEntry = function(service, index) {
|
||||
var removedElement = service.Hosts.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'Hosts', service.Hosts);
|
||||
}
|
||||
};
|
||||
$scope.updateHostsEntry = function(service, entry) {
|
||||
updateServiceArray(service, 'Hosts', service.Hosts);
|
||||
};
|
||||
|
||||
$scope.cancelChanges = function cancelChanges(service, keys) {
|
||||
if (keys) { // clean out the keys only from the list of modified keys
|
||||
keys.forEach(function(key) {
|
||||
var index = previousServiceValues.indexOf(key);
|
||||
if (index >= 0) {
|
||||
previousServiceValues.splice(index, 1);
|
||||
}
|
||||
});
|
||||
} else { // clean out all changes
|
||||
keys = Object.keys(service);
|
||||
previousServiceValues = [];
|
||||
}
|
||||
keys.forEach(function(attribute) {
|
||||
service[attribute] = originalService[attribute]; // reset service values
|
||||
});
|
||||
service.hasChanges = false;
|
||||
};
|
||||
|
||||
$scope.hasChanges = function(service, elements) {
|
||||
var hasChanges = false;
|
||||
elements.forEach(function(key) {
|
||||
hasChanges = hasChanges || (previousServiceValues.indexOf(key) >= 0);
|
||||
});
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
$scope.updateService = function updateService(service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Name = service.Name;
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
|
||||
config.TaskTemplate.ContainerSpec.Env = ServiceHelper.translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
|
||||
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) : [];
|
||||
config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : [];
|
||||
|
||||
if (service.Mode === 'replicated') {
|
||||
config.Mode.Replicated.Replicas = service.Replicas;
|
||||
}
|
||||
config.TaskTemplate.ContainerSpec.Mounts = service.ServiceMounts;
|
||||
if (typeof config.TaskTemplate.Placement === 'undefined') {
|
||||
config.TaskTemplate.Placement = {};
|
||||
}
|
||||
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
|
||||
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences);
|
||||
|
||||
// Round memory values to 0.125 and convert MB to B
|
||||
var memoryLimit = (Math.round(service.LimitMemoryBytes * 8) / 8).toFixed(3);
|
||||
memoryLimit *= 1024 * 1024;
|
||||
var memoryReservation = (Math.round(service.ReservationMemoryBytes * 8) / 8).toFixed(3);
|
||||
memoryReservation *= 1024 * 1024;
|
||||
config.TaskTemplate.Resources = {
|
||||
Limits: {
|
||||
NanoCPUs: service.LimitNanoCPUs * 1000000000,
|
||||
MemoryBytes: memoryLimit
|
||||
},
|
||||
Reservations: {
|
||||
NanoCPUs: service.ReservationNanoCPUs * 1000000000,
|
||||
MemoryBytes: memoryReservation
|
||||
}
|
||||
};
|
||||
|
||||
config.UpdateConfig = {
|
||||
Parallelism: service.UpdateParallelism,
|
||||
Delay: ServiceHelper.translateHumanDurationToNanos(service.UpdateDelay) || 0,
|
||||
FailureAction: service.UpdateFailureAction,
|
||||
Order: service.UpdateOrder
|
||||
};
|
||||
|
||||
if ($scope.hasChanges(service, ['RestartCondition', 'RestartDelay', 'RestartMaxAttempts', 'RestartWindow'])){
|
||||
config.TaskTemplate.RestartPolicy = {
|
||||
Condition: service.RestartCondition,
|
||||
Delay: ServiceHelper.translateHumanDurationToNanos(service.RestartDelay) || 5000000000,
|
||||
MaxAttempts: service.RestartMaxAttempts,
|
||||
Window: ServiceHelper.translateHumanDurationToNanos(service.RestartWindow) || 0
|
||||
};
|
||||
}
|
||||
|
||||
config.TaskTemplate.LogDriver = null;
|
||||
if (service.LogDriverName) {
|
||||
config.TaskTemplate.LogDriver = { Name: service.LogDriverName };
|
||||
if (service.LogDriverName !== 'none') {
|
||||
var logOpts = ServiceHelper.translateKeyValueToLogDriverOpts(service.LogDriverOpts);
|
||||
if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) {
|
||||
config.TaskTemplate.LogDriver.Options = logOpts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (service.Ports) {
|
||||
service.Ports.forEach(function (binding) {
|
||||
if (binding.PublishedPort === null || binding.PublishedPort === '') {
|
||||
delete binding.PublishedPort;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
config.EndpointSpec = {
|
||||
Mode: (config.EndpointSpec && config.EndpointSpec.Mode) || 'vip',
|
||||
Ports: service.Ports
|
||||
};
|
||||
|
||||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||
if (data.message && data.message.match(/^rpc error:/)) {
|
||||
Notifications.error(data.message, 'Error');
|
||||
} else {
|
||||
Notifications.success('Service successfully updated', 'Service updated');
|
||||
}
|
||||
$scope.cancelChanges({});
|
||||
initView();
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to update service');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeService = function() {
|
||||
ModalService.confirmDeletion(
|
||||
'Do you want to remove this service? All the containers associated to this service will be removed too.',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
removeService();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function removeService() {
|
||||
$scope.state.deletionInProgress = true;
|
||||
ServiceService.remove($scope.service)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Service successfully deleted');
|
||||
$state.go('docker.services', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove service');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.deletionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.forceUpdateService = function(service) {
|
||||
ModalService.confirmServiceForceUpdate(
|
||||
'Do you want to force update this service? All the tasks associated to the selected service(s) will be recreated.',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
forceUpdateService(service);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function forceUpdateService(service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
// As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random
|
||||
// value or an increment of the counter value to force an update.
|
||||
config.TaskTemplate.ForceUpdate++;
|
||||
$scope.state.updateInProgress = true;
|
||||
ServiceService.update(service, config)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Service successfully updated', service.Name);
|
||||
$scope.cancelChanges({});
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to force update service', service.Name);
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.updateInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
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.LogDriverOpts = ServiceHelper.translateLogDriverOptsToKeyValue(service.LogDriverOpts);
|
||||
service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels);
|
||||
service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels);
|
||||
service.ServiceMounts = angular.copy(service.Mounts);
|
||||
service.ServiceConstraints = ServiceHelper.translateConstraintsToKeyValue(service.Constraints);
|
||||
service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
|
||||
service.Hosts = ServiceHelper.translateHostsEntriesToHostnameIP(service.Hosts);
|
||||
}
|
||||
|
||||
function transformResources(service) {
|
||||
service.LimitNanoCPUs = service.LimitNanoCPUs / 1000000000 || 0;
|
||||
service.ReservationNanoCPUs = service.ReservationNanoCPUs / 1000000000 || 0;
|
||||
service.LimitMemoryBytes = service.LimitMemoryBytes / 1024 / 1024 || 0;
|
||||
service.ReservationMemoryBytes = service.ReservationMemoryBytes / 1024 / 1024 || 0;
|
||||
}
|
||||
|
||||
function transformDurations(service) {
|
||||
service.RestartDelay = ServiceHelper.translateNanosToHumanDuration(service.RestartDelay) || '5s';
|
||||
service.RestartWindow = ServiceHelper.translateNanosToHumanDuration(service.RestartWindow) || '0s';
|
||||
service.UpdateDelay = ServiceHelper.translateNanosToHumanDuration(service.UpdateDelay) || '0s';
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
ServiceService.service($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var service = data;
|
||||
$scope.isUpdating = $scope.lastVersion >= service.Version;
|
||||
if (!$scope.isUpdating) {
|
||||
$scope.lastVersion = service.Version;
|
||||
}
|
||||
|
||||
transformResources(service);
|
||||
translateServiceArrays(service);
|
||||
transformDurations(service);
|
||||
$scope.service = service;
|
||||
originalService = angular.copy(service);
|
||||
|
||||
return $q.all({
|
||||
tasks: TaskService.tasks({ service: [service.Name] }),
|
||||
nodes: NodeService.nodes(),
|
||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||
configs: apiVersion >= 1.30 ? ConfigService.configs() : [],
|
||||
availableImages: ImageService.images(),
|
||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25)
|
||||
});
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.tasks = data.tasks;
|
||||
$scope.nodes = data.nodes;
|
||||
$scope.configs = data.configs;
|
||||
$scope.secrets = data.secrets;
|
||||
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
||||
|
||||
// Set max cpu value
|
||||
var maxCpus = 0;
|
||||
for (var n in data.nodes) {
|
||||
if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
|
||||
maxCpus = data.nodes[n].CPUs;
|
||||
}
|
||||
}
|
||||
if (maxCpus > 0) {
|
||||
$scope.state.sliderMaxCpu = maxCpus / 1000000000;
|
||||
} else {
|
||||
$scope.state.sliderMaxCpu = 32;
|
||||
}
|
||||
|
||||
// Default values
|
||||
$scope.state.addSecret = {override: false};
|
||||
|
||||
$timeout(function() {
|
||||
$anchorScroll();
|
||||
});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.secrets = [];
|
||||
$scope.configs = [];
|
||||
Notifications.error('Failure', err, 'Unable to retrieve service details');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.updateServiceAttribute = function updateServiceAttribute(service, name) {
|
||||
if (service[name] !== originalService[name] || !(name in originalService)) {
|
||||
service.hasChanges = true;
|
||||
}
|
||||
previousServiceValues.push(name);
|
||||
};
|
||||
|
||||
function updateServiceArray(service, name) {
|
||||
previousServiceValues.push(name);
|
||||
service.hasChanges = true;
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
78
app/docker/views/services/logs/serviceLogsController.js
Normal file
78
app/docker/views/services/logs/serviceLogsController.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ServiceLogsController', ['$scope', '$transition$', '$anchorScroll', 'ServiceLogs', 'Service',
|
||||
function ($scope, $transition$, $anchorScroll, ServiceLogs, Service) {
|
||||
$scope.state = {};
|
||||
$scope.state.displayTimestampsOut = false;
|
||||
$scope.state.displayTimestampsErr = false;
|
||||
$scope.stdout = '';
|
||||
$scope.stderr = '';
|
||||
$scope.tailLines = 2000;
|
||||
|
||||
function getLogs() {
|
||||
getLogsStdout();
|
||||
getLogsStderr();
|
||||
}
|
||||
|
||||
function getLogsStderr() {
|
||||
ServiceLogs.get($transition$.params().id, {
|
||||
stdout: 0,
|
||||
stderr: 1,
|
||||
timestamps: $scope.state.displayTimestampsErr,
|
||||
tail: $scope.tailLines
|
||||
}, function (data, status, headers, config) {
|
||||
// Replace carriage returns with newlines to clean up output
|
||||
data = data.replace(/[\r]/g, '\n');
|
||||
// Strip 8 byte header from each line of output
|
||||
data = data.substring(8);
|
||||
data = data.replace(/\n(.{8})/g, '\n');
|
||||
$scope.stderr = data;
|
||||
});
|
||||
}
|
||||
|
||||
function getLogsStdout() {
|
||||
ServiceLogs.get($transition$.params().id, {
|
||||
stdout: 1,
|
||||
stderr: 0,
|
||||
timestamps: $scope.state.displayTimestampsOut,
|
||||
tail: $scope.tailLines
|
||||
}, function (data, status, headers, config) {
|
||||
// Replace carriage returns with newlines to clean up output
|
||||
data = data.replace(/[\r]/g, '\n');
|
||||
// Strip 8 byte header from each line of output
|
||||
data = data.substring(8);
|
||||
data = data.replace(/\n(.{8})/g, '\n');
|
||||
$scope.stdout = data;
|
||||
});
|
||||
}
|
||||
|
||||
function getService() {
|
||||
Service.get({id: $transition$.params().id}, function (d) {
|
||||
$scope.service = d;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve service info');
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
getService();
|
||||
getLogs();
|
||||
|
||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
// clearing interval when view changes
|
||||
clearInterval(logIntervalId);
|
||||
});
|
||||
|
||||
$scope.toggleTimestampsOut = function () {
|
||||
getLogsStdout();
|
||||
};
|
||||
|
||||
$scope.toggleTimestampsErr = function () {
|
||||
getLogsStderr();
|
||||
};
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
}]);
|
54
app/docker/views/services/logs/servicelogs.html
Normal file
54
app/docker/views/services/logs/servicelogs.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Service logs"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.ID})">{{ service.Spec.Name }}</a> > Logs
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-list-alt"></i>
|
||||
</div>
|
||||
<div class="title">{{ service.Spec.Name }}</div>
|
||||
<div class="comment">Name</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
|
||||
<rd-widget-taskbar>
|
||||
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
|
||||
<label for="displayAllTsOut">Display timestamps</label>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="panel-body">
|
||||
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
|
||||
<rd-widget-taskbar>
|
||||
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
|
||||
<label for="displayAllTsErr">Display timestamps</label>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="panel-body">
|
||||
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
24
app/docker/views/services/services.html
Normal file
24
app/docker/views/services/services.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Service list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker.services" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Services</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<services-datatable
|
||||
title="Services" title-icon="fa-list-alt"
|
||||
dataset="services" table-key="services"
|
||||
order-by="Name" show-text-filter="true"
|
||||
show-ownership-column="applicationState.application.authentication"
|
||||
remove-action="removeAction"
|
||||
scale-action="scaleAction"
|
||||
force-update-action="forceUpdateAction"
|
||||
swarm-manager-ip="swarmManagerIP"
|
||||
show-force-update-button="applicationState.endpoint.apiVersion >= 1.25"
|
||||
></services-datatable>
|
||||
</div>
|
||||
</div>
|
112
app/docker/views/services/servicesController.js
Normal file
112
app/docker/views/services/servicesController.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ServicesController', ['$q', '$scope', '$state', 'Service', 'ServiceService', 'ServiceHelper', 'Notifications', 'Task', 'Node', 'NodeHelper', 'ModalService',
|
||||
function ($q, $scope, $state, Service, ServiceService, ServiceHelper, Notifications, Task, Node, NodeHelper, ModalService) {
|
||||
|
||||
$scope.scaleAction = function scaleService(service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Mode.Replicated.Replicas = service.Replicas;
|
||||
ServiceService.update(service, config)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Service successfully scaled', 'New replica count: ' + service.Replicas);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to scale service');
|
||||
service.Scale = false;
|
||||
service.Replicas = service.ReplicaCount;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.forceUpdateAction = function(selectedItems) {
|
||||
ModalService.confirmServiceForceUpdate(
|
||||
'Do you want to force update of selected service(s)? All the tasks associated to the selected service(s) will be recreated.',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
forceUpdateServices(selectedItems);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function forceUpdateServices(services) {
|
||||
var actionCount = services.length;
|
||||
angular.forEach(services, function (service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
// As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random
|
||||
// value or an increment of the counter value to force an update.
|
||||
config.TaskTemplate.ForceUpdate++;
|
||||
ServiceService.update(service, config)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Service successfully updated', service.Name);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to force update service', service.Name);
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.removeAction = function(selectedItems) {
|
||||
ModalService.confirmDeletion(
|
||||
'Do you want to remove the selected service(s)? All the containers associated to the selected service(s) will be removed too.',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
removeServices(selectedItems);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function removeServices(services) {
|
||||
var actionCount = services.length;
|
||||
angular.forEach(services, function (service) {
|
||||
ServiceService.remove(service)
|
||||
.then(function success() {
|
||||
Notifications.success('Service successfully removed', service.Name);
|
||||
var index = $scope.services.indexOf(service);
|
||||
$scope.services.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove service');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$q.all({
|
||||
services: Service.query({}).$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 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, runningTasks, allTasks, taskNodes);
|
||||
});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.services = [];
|
||||
Notifications.error('Failure', err, 'Unable to retrieve services');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
Loading…
Add table
Add a link
Reference in a new issue