mirror of
https://github.com/portainer/portainer.git
synced 2025-07-27 09:19:39 +02:00
feat(stacks): support compose v2.0 stack (#1963)
This commit is contained in:
parent
ef15cd30eb
commit
e3d564325b
174 changed files with 7898 additions and 5849 deletions
|
@ -0,0 +1,15 @@
|
|||
<div class="actionBar">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button ng-if="$ctrl.showUpdateAction" type="button" class="btn btn-sm btn-primary"
|
||||
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.updateAction($ctrl.selectedItems)">
|
||||
<i class="fa fa-sync space-right" aria-hidden="true"></i>Update
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.services.new" ng-if="$ctrl.showAddAction">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add service
|
||||
</button>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
angular.module('portainer.docker').component('servicesDatatableActions', {
|
||||
templateUrl: 'app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html',
|
||||
controller: 'ServicesDatatableActionsController',
|
||||
bindings: {
|
||||
selectedItems: '=',
|
||||
selectedItemCount: '=',
|
||||
showUpdateAction: '<',
|
||||
showAddAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ServicesDatatableActionsController', ['$state', 'ServiceService', 'ServiceHelper', 'Notifications', 'ModalService',
|
||||
function ($state, ServiceService, ServiceHelper, Notifications, ModalService) {
|
||||
|
||||
this.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;
|
||||
});
|
||||
};
|
||||
|
||||
this.updateAction = 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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.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 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove service');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}]);
|
|
@ -11,21 +11,12 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button ng-if="$ctrl.showForceUpdateButton" type="button" class="btn btn-sm btn-primary"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.forceUpdateAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-sync space-right" aria-hidden="true"></i>Update
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.services.new">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add service
|
||||
</button>
|
||||
</div>
|
||||
<services-datatable-actions
|
||||
selected-items="$ctrl.state.selectedItems"
|
||||
selected-item-count="$ctrl.state.selectedItemCount"
|
||||
show-add-action="$ctrl.showAddAction"
|
||||
show-update-action="$ctrl.showUpdateAction"
|
||||
></services-datatable-actions>
|
||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
|
@ -34,18 +25,23 @@
|
|||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<th style="width:55px;">
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.expandAll()">
|
||||
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<th ng-if="$ctrl.showStackColumn">
|
||||
<a ng-click="$ctrl.changeOrderBy('StackName')">
|
||||
Stack
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
|
||||
|
@ -90,34 +86,38 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<tr ng-click="$ctrl.expandItem(item, !item.Expanded)" dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}" class="interactive">
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="docker.services.service({id: item.Id})">{{ item.Name }}</a>
|
||||
<a><i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i></a>
|
||||
</td>
|
||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td>{{ item.Image | hideshasum }}</td>
|
||||
<td>
|
||||
{{ item.Mode }} <code>{{ item.Running }}</code> / <code>{{ item.Replicas }}</code>
|
||||
<a ui-sref="docker.services.service({id: item.Id})" ng-click="$event.stopPropagation()">{{ item.Name }}</a>
|
||||
</td>
|
||||
<td ng-if="$ctrl.showStackColumn">{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td>{{ item.Image | hideshasum }}</td>
|
||||
<td ng-controller="ServicesDatatableActionsController as actionCtrl">
|
||||
{{ item.Mode }}
|
||||
<code>{{ item.Tasks | runningtaskscount }}</code> / <code>{{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount) }}</code>
|
||||
<span ng-if="item.Mode === 'replicated' && !item.Scale">
|
||||
<a class="interactive" ng-click="item.Scale = true; item.ReplicaCount = item.Replicas;">
|
||||
<a class="interactive" ng-click="item.Scale = true; item.ReplicaCount = item.Replicas; $event.stopPropagation();">
|
||||
<i class="fa fa-arrows-alt-v" aria-hidden="true"></i> Scale
|
||||
</a>
|
||||
</span>
|
||||
<span ng-if="item.Mode === 'replicated' && item.Scale">
|
||||
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" auto-focus/>
|
||||
<a class="interactive" ng-click="item.Scale = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="$ctrl.scaleAction(item)"><i class="fa fa-check-square"></i></a>
|
||||
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="actionCtrl.scaleAction(item)" auto-focus ng-click="$event.stopPropagation();"/>
|
||||
<a class="interactive" ng-click="item.Scale = false; $event.stopPropagation();"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="actionCtrl.scaleAction(item); $event.stopPropagation();"><i class="fa fa-check-square"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="item.Ports && item.Ports.length > 0 && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.publicUrl }}:{{ p.PublishedPort }}" target="_blank">
|
||||
<a ng-if="item.Ports && item.Ports.length > 0 && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.state.publicUrl }}:{{ p.PublishedPort }}" target="_blank" ng-click="$event.stopPropagation();">
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
|
||||
</a>
|
||||
<span ng-if="!item.Ports || item.Ports.length === 0 || !$ctrl.swarmManagerIp" >-</span>
|
||||
<span ng-if="!item.Ports || item.Ports.length === 0">-</span>
|
||||
</td>
|
||||
<td>{{ item.UpdatedAt | getisodate }}</td>
|
||||
<td ng-if="$ctrl.showOwnershipColumn">
|
||||
|
@ -127,11 +127,26 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr dir-paginate-end ng-show="item.Expanded">
|
||||
<td></td>
|
||||
<td colspan="8">
|
||||
<service-tasks-datatable
|
||||
dataset="item.Tasks"
|
||||
service-id="item.Id"
|
||||
table-key="service-tasks"
|
||||
order-by="Status.State"
|
||||
nodes="$ctrl.nodes"
|
||||
agent-proxy="$ctrl.agentProxy"
|
||||
show-task-logs-button="$ctrl.showTaskLogsButton"
|
||||
text-filter="$ctrl.state.textFilter"
|
||||
><service-tasks-datatable>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="7" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="8" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="7" class="text-center text-muted">No service available.</td>
|
||||
<td colspan="8" class="text-center text-muted">No service available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker').component('servicesDatatable', {
|
||||
templateUrl: 'app/docker/components/datatables/services-datatable/servicesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
controller: 'ServicesDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
|
@ -8,12 +8,13 @@ angular.module('portainer.docker').component('servicesDatatable', {
|
|||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
nodes: '<',
|
||||
agentProxy: '<',
|
||||
showTextFilter: '<',
|
||||
showOwnershipColumn: '<',
|
||||
removeAction: '<',
|
||||
scaleAction: '<',
|
||||
publicUrl: '<',
|
||||
forceUpdateAction: '<',
|
||||
showForceUpdateButton: '<'
|
||||
showUpdateAction: '<',
|
||||
showAddAction: '<',
|
||||
showStackColumn: '<',
|
||||
showTaskLogsButton: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ServicesDatatableController', ['PaginationService', 'DatatableService', 'EndpointProvider',
|
||||
function (PaginationService, DatatableService, EndpointProvider) {
|
||||
|
||||
var ctrl = this;
|
||||
|
||||
this.state = {
|
||||
selectAll: false,
|
||||
expandAll: false,
|
||||
orderBy: this.orderBy,
|
||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
||||
displayTextFilter: false,
|
||||
selectedItemCount: 0,
|
||||
selectedItems: [],
|
||||
expandedItems: [],
|
||||
publicURL: EndpointProvider.endpointPublicURL()
|
||||
};
|
||||
|
||||
this.changeOrderBy = function(orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
this.state.orderBy = orderField;
|
||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
||||
};
|
||||
|
||||
this.selectItem = function(item) {
|
||||
if (item.Checked) {
|
||||
this.state.selectedItemCount++;
|
||||
this.state.selectedItems.push(item);
|
||||
} else {
|
||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
||||
this.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
this.selectAll = function() {
|
||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
||||
var item = this.state.filteredDataSet[i];
|
||||
if (item.Checked !== this.state.selectAll) {
|
||||
item.Checked = this.state.selectAll;
|
||||
this.selectItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.expandAll = function() {
|
||||
this.state.expandAll = !this.state.expandAll;
|
||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
||||
var item = this.state.filteredDataSet[i];
|
||||
this.expandItem(item, this.state.expandAll);
|
||||
}
|
||||
};
|
||||
|
||||
this.changePaginationLimit = function() {
|
||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||
};
|
||||
|
||||
this.updateDisplayTextFilter = function() {
|
||||
this.state.displayTextFilter = !this.state.displayTextFilter;
|
||||
if (!this.state.displayTextFilter) {
|
||||
delete this.state.textFilter;
|
||||
}
|
||||
};
|
||||
|
||||
this.expandItem = function(item, expanded) {
|
||||
item.Expanded = expanded;
|
||||
if (item.Expanded) {
|
||||
if (this.state.expandedItems.indexOf(item.Id) === -1) {
|
||||
this.state.expandedItems.push(item.Id);
|
||||
}
|
||||
} else {
|
||||
var index = this.state.expandedItems.indexOf(item.Id);
|
||||
if (index > -1) {
|
||||
this.state.expandedItems.splice(index, 1);
|
||||
}
|
||||
}
|
||||
DatatableService.setDataTableExpandedItems(this.tableKey, this.state.expandedItems);
|
||||
};
|
||||
|
||||
function expandPreviouslyExpandedItem(item, storedExpandedItems) {
|
||||
var expandedItem = _.find(storedExpandedItems, function(storedId) {
|
||||
return item.Id === storedId;
|
||||
});
|
||||
|
||||
if (expandedItem) {
|
||||
ctrl.expandItem(item, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.expandItems = function(storedExpandedItems) {
|
||||
var expandedItemCount = 0;
|
||||
this.state.expandedItems = storedExpandedItems;
|
||||
|
||||
for (var i = 0; i < this.dataset.length; i++) {
|
||||
var item = this.dataset[i];
|
||||
expandPreviouslyExpandedItem(item, storedExpandedItems);
|
||||
if (item.Expanded) {
|
||||
++expandedItemCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedItemCount === this.dataset.length) {
|
||||
this.state.expandAll = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
setDefaults(this);
|
||||
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
|
||||
var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
|
||||
if (storedExpandedItems !== null) {
|
||||
this.expandItems(storedExpandedItems);
|
||||
}
|
||||
};
|
||||
|
||||
function setDefaults(ctrl) {
|
||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
||||
}
|
||||
}]);
|
Loading…
Add table
Add a link
Reference in a new issue