mirror of https://github.com/portainer/portainer
				
				
				
			feat(networks): group networks for swarm endpoints (#3028)
* feat(networks): group networks for swarm endpoints * fix(networks): display error on networks with 1 subpull/3086/head
							parent
							
								
									552c897b3b
								
							
						
					
					
						commit
						c12ce5a5c7
					
				| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
<td ng-if="allowCheckbox">
 | 
			
		||||
  <span class="md-checkbox" ng-if="!parentCtrl.offlineMode">
 | 
			
		||||
    <input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="parentCtrl.selectItem(item)" ng-disabled="parentCtrl.disableRemove(item)"/>
 | 
			
		||||
    <label for="select_{{ $index }}"></label>
 | 
			
		||||
  </span>
 | 
			
		||||
  <a ng-if="parentCtrl.itemCanExpand(item)" ng-click="parentCtrl.expandItem(item, !item.Expanded)"><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 ng-if="!allowCheckbox"></td>
 | 
			
		||||
<td>
 | 
			
		||||
  <a ng-if="!parentCtrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
 | 
			
		||||
  <span ng-if="parentCtrl.offlineMode">{{ item.Name | truncate:40 }}</span>
 | 
			
		||||
</td>
 | 
			
		||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
 | 
			
		||||
<td>{{ item.Scope }}</td>
 | 
			
		||||
<td>{{ item.Driver }}</td>
 | 
			
		||||
<td>{{ item.Attachable }}</td>
 | 
			
		||||
<td>{{ item.Internal }}</td>
 | 
			
		||||
<td>{{ item.IPAM.Driver }}</td>
 | 
			
		||||
<td>{{ item.IPAM.Config[0].Subnet ? item.IPAM.Config[0].Subnet : '-' }}</td>
 | 
			
		||||
<td>{{ item.IPAM.Config[0].Gateway ? item.IPAM.Config[0].Gateway : '-' }}</td>
 | 
			
		||||
<td ng-if="parentCtrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
 | 
			
		||||
<td ng-if="parentCtrl.showOwnershipColumn">
 | 
			
		||||
  <span>
 | 
			
		||||
    <i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
 | 
			
		||||
    {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
 | 
			
		||||
  </span>
 | 
			
		||||
</td>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
angular.module('portainer.docker')
 | 
			
		||||
.directive('networkRowContent', [function networkRowContent() {
 | 
			
		||||
  var directive = {
 | 
			
		||||
    templateUrl: './networkRowContent.html',
 | 
			
		||||
    restrict: 'A',
 | 
			
		||||
    transclude: true,
 | 
			
		||||
    scope: {
 | 
			
		||||
      item: '<',
 | 
			
		||||
      parentCtrl: '<',
 | 
			
		||||
      allowCheckbox: '<',
 | 
			
		||||
      allowExpand: '<'
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  return directive;
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			@ -61,11 +61,16 @@
 | 
			
		|||
        <table class="table table-hover nowrap-cells">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>
 | 
			
		||||
              <th style="width:55px;">
 | 
			
		||||
                <span class="md-checkbox" ng-if="!$ctrl.offlineMode">
 | 
			
		||||
                  <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()" ng-if="$ctrl.hasExpandableItems()">
 | 
			
		||||
                  <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>
 | 
			
		||||
| 
						 | 
				
			
			@ -145,30 +150,11 @@
 | 
			
		|||
            </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}">
 | 
			
		||||
              <td>
 | 
			
		||||
                <span class="md-checkbox" ng-if="!$ctrl.offlineMode">
 | 
			
		||||
                  <input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableRemove(item)"/>
 | 
			
		||||
                  <label for="select_{{ $index }}"></label>
 | 
			
		||||
                </span>
 | 
			
		||||
                <a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
 | 
			
		||||
                <span ng-if="$ctrl.offlineMode">{{ item.Name | truncate:40 }}</span>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>{{ item.StackName ? item.StackName : '-' }}</td>
 | 
			
		||||
              <td>{{ item.Scope }}</td>
 | 
			
		||||
              <td>{{ item.Driver }}</td>
 | 
			
		||||
              <td>{{ item.Attachable }}</td>
 | 
			
		||||
              <td>{{ item.Internal }}</td>
 | 
			
		||||
              <td>{{ item.IPAM.Driver }}</td>
 | 
			
		||||
              <td>{{ item.IPAM.Config[0].Subnet ? item.IPAM.Config[0].Subnet : '-' }}</td>
 | 
			
		||||
              <td>{{ item.IPAM.Config[0].Gateway ? item.IPAM.Config[0].Gateway : '-' }}</td>
 | 
			
		||||
              <td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
 | 
			
		||||
              <td ng-if="$ctrl.showOwnershipColumn">
 | 
			
		||||
                <span>
 | 
			
		||||
                  <i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
 | 
			
		||||
                  {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
 | 
			
		||||
                </span>
 | 
			
		||||
              </td>
 | 
			
		||||
            <tr 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}" network-row-content item="item" parent-ctrl="$ctrl" allow-checkbox="true">
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr dir-paginate-end ng-show="item.Expanded" ng-repeat="it in item.Subs" style="background: #d5e8f3;"
 | 
			
		||||
              network-row-content item="it" parent-ctrl="$ctrl">
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr ng-if="!$ctrl.dataset">
 | 
			
		||||
              <td colspan="9" class="text-center text-muted">Loading...</td>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
import _ from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
angular.module('portainer.docker')
 | 
			
		||||
  .controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService',
 | 
			
		||||
    function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) {
 | 
			
		||||
| 
						 | 
				
			
			@ -8,6 +10,10 @@ angular.module('portainer.docker')
 | 
			
		|||
        return PREDEFINED_NETWORKS.includes(item.Name);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.state = Object.assign(this.state, {
 | 
			
		||||
        expandedItems: []
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      /**
 | 
			
		||||
       * Do not allow PREDEFINED_NETWORKS to be selected
 | 
			
		||||
       */
 | 
			
		||||
| 
						 | 
				
			
			@ -47,5 +53,26 @@ angular.module('portainer.docker')
 | 
			
		|||
        }
 | 
			
		||||
        this.onSettingsRepeaterChange();
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.expandItem = function(item, expanded) {
 | 
			
		||||
        item.Expanded = expanded;
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.itemCanExpand = function(item) {
 | 
			
		||||
        return item.Subs.length > 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.hasExpandableItems = function() {
 | 
			
		||||
        return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.expandAll = function() {
 | 
			
		||||
        this.state.expandAll = !this.state.expandAll;
 | 
			
		||||
        _.forEach(this.state.filteredDataSet, (item) => {
 | 
			
		||||
          if (this.itemCanExpand(item)) {
 | 
			
		||||
            this.expandItem(item, this.state.expandAll);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
  }
 | 
			
		||||
]);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
import _ from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
angular.module('portainer.docker')
 | 
			
		||||
.controller('NetworksController', ['$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper', 'EndpointProvider',
 | 
			
		||||
function ($scope, $state, NetworkService, Notifications, HttpRequestHelper, EndpointProvider) {
 | 
			
		||||
.controller('NetworksController', ['$q', '$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper', 'EndpointProvider', 'AgentService',
 | 
			
		||||
function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, EndpointProvider, AgentService) {
 | 
			
		||||
 | 
			
		||||
  $scope.removeAction = function (selectedItems) {
 | 
			
		||||
    var actionCount = selectedItems.length;
 | 
			
		||||
| 
						 | 
				
			
			@ -28,13 +30,43 @@ function ($scope, $state, NetworkService, Notifications, HttpRequestHelper, Endp
 | 
			
		|||
 | 
			
		||||
  $scope.getNetworks = getNetworks;
 | 
			
		||||
 | 
			
		||||
  function groupSwarmNetworksManagerNodesFirst(networks, agents) {
 | 
			
		||||
    const getRole = (item) => _.find(agents, (agent) => agent.NodeName === item.NodeName).NodeRole;
 | 
			
		||||
 | 
			
		||||
    const nonSwarmNetworks = _.remove(networks, (item) => item.Scope !== 'swarm')
 | 
			
		||||
    const grouped = _.toArray(_.groupBy(networks, (item) => item.Id));
 | 
			
		||||
    const sorted = _.map(grouped, (arr) => _.sortBy(arr, (item) => getRole(item)));
 | 
			
		||||
    const arr = _.map(sorted, (a) => {
 | 
			
		||||
      const item = a[0];
 | 
			
		||||
      for (let i = 1; i < a.length; i++) {
 | 
			
		||||
        item.Subs.push(a[i]);
 | 
			
		||||
      }
 | 
			
		||||
      return item;
 | 
			
		||||
    });
 | 
			
		||||
    const res = _.concat(arr, ...nonSwarmNetworks);
 | 
			
		||||
    return res;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getNetworks() {
 | 
			
		||||
    NetworkService.networks(true, true, true)
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      $scope.networks = data;
 | 
			
		||||
    const req = {
 | 
			
		||||
      networks: NetworkService.networks(true, true, true)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
 | 
			
		||||
      req.agents = AgentService.agents();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $q.all(req)
 | 
			
		||||
    .then((data) => {
 | 
			
		||||
      $scope.offlineMode = EndpointProvider.offlineMode();
 | 
			
		||||
      const networks = _.forEach(data.networks, (item) => item.Subs = []);
 | 
			
		||||
      if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
 | 
			
		||||
        $scope.networks = groupSwarmNetworksManagerNodesFirst(data.networks, data.agents);
 | 
			
		||||
      } else {
 | 
			
		||||
        $scope.networks = networks;
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
    .catch((err) => {
 | 
			
		||||
      $scope.networks = [];
 | 
			
		||||
      Notifications.error('Failure', err, 'Unable to retrieve networks');
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue