1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-09 15:55:23 +02:00

refactor(app): introduce new project structure for the frontend (#1623)

This commit is contained in:
Anthony Lapenna 2018-02-01 13:27:52 +01:00 committed by GitHub
parent e6422a6d75
commit 27dceadba1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
354 changed files with 1518 additions and 1755 deletions

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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> &gt; <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 &amp; published ports</a></li>
<li><a href ng-click="goToItem('service-resources')">Resource limits &amp; 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 &amp; 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>

View 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();
}]);