mirror of https://github.com/portainer/portainer
				
				
				
			refactor(containers): migrate resources tab to react [EE-5214] (#10355)
							parent
							
								
									ec091efe3b
								
							
						
					
					
						commit
						ffac83864d
					
				| 
						 | 
				
			
			@ -25,6 +25,11 @@ import {
 | 
			
		|||
  NetworkTab,
 | 
			
		||||
  type NetworkTabValues,
 | 
			
		||||
} from '@/react/docker/containers/CreateView/NetworkTab';
 | 
			
		||||
import {
 | 
			
		||||
  ResourcesTab,
 | 
			
		||||
  resourcesTabUtils,
 | 
			
		||||
  type ResourcesTabValues,
 | 
			
		||||
} from '@/react/docker/containers/CreateView/ResourcesTab';
 | 
			
		||||
 | 
			
		||||
const ngModule = angular
 | 
			
		||||
  .module('portainer.docker.react.components.containers', [])
 | 
			
		||||
| 
						 | 
				
			
			@ -70,3 +75,19 @@ withFormValidation<ComponentProps<typeof NetworkTab>, NetworkTabValues>(
 | 
			
		|||
  [],
 | 
			
		||||
  networkTabUtils.validation
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
withFormValidation<ComponentProps<typeof ResourcesTab>, ResourcesTabValues>(
 | 
			
		||||
  ngModule,
 | 
			
		||||
  withUIRouter(withReactQuery(ResourcesTab)),
 | 
			
		||||
  'dockerCreateContainerResourcesTab',
 | 
			
		||||
  [
 | 
			
		||||
    'allowPrivilegedMode',
 | 
			
		||||
    'isDevicesFieldVisible',
 | 
			
		||||
    'isInitFieldVisible',
 | 
			
		||||
    'isSysctlFieldVisible',
 | 
			
		||||
    'isDuplicate',
 | 
			
		||||
    'isImageInvalid',
 | 
			
		||||
    'redeploy',
 | 
			
		||||
  ],
 | 
			
		||||
  resourcesTabUtils.validation
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ import { StackContainersDatatable } from '@/react/common/stacks/ItemView/StackCo
 | 
			
		|||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
 | 
			
		||||
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
 | 
			
		||||
import { TemplateListSortAngular } from '@/react/docker/app-templates/TemplateListSort';
 | 
			
		||||
import { Gpu } from '@/react/docker/containers/CreateView/Gpu';
 | 
			
		||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
 | 
			
		||||
import { withReactQuery } from '@/react-tools/withReactQuery';
 | 
			
		||||
import { withUIRouter } from '@/react-tools/withUIRouter';
 | 
			
		||||
| 
						 | 
				
			
			@ -57,17 +56,6 @@ const ngModule = angular
 | 
			
		|||
      ['environment', 'stackName']
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
  .component(
 | 
			
		||||
    'gpu',
 | 
			
		||||
    r2a(Gpu, [
 | 
			
		||||
      'values',
 | 
			
		||||
      'onChange',
 | 
			
		||||
      'gpus',
 | 
			
		||||
      'usedGpus',
 | 
			
		||||
      'usedAllGpus',
 | 
			
		||||
      'enableGpuManagement',
 | 
			
		||||
    ])
 | 
			
		||||
  )
 | 
			
		||||
  .component(
 | 
			
		||||
    'gpusList',
 | 
			
		||||
    r2a(withControlledInput(GpusList), ['value', 'onChange'])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ import { ContainerDetailsViewModel } from '@/docker/models/container';
 | 
			
		|||
import './createcontainer.css';
 | 
			
		||||
import { envVarsTabUtils } from '@/react/docker/containers/CreateView/EnvVarsTab';
 | 
			
		||||
import { getContainers } from '@/react/docker/containers/queries/containers';
 | 
			
		||||
import { resourcesTabUtils } from '@/react/docker/containers/CreateView/ResourcesTab';
 | 
			
		||||
 | 
			
		||||
angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		||||
  '$q',
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +66,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
    endpoint
 | 
			
		||||
  ) {
 | 
			
		||||
    $scope.create = create;
 | 
			
		||||
    $scope.update = update;
 | 
			
		||||
    $scope.endpoint = endpoint;
 | 
			
		||||
    $scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
 | 
			
		||||
    $scope.formValues = {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,18 +84,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
      DnsPrimary: '',
 | 
			
		||||
      DnsSecondary: '',
 | 
			
		||||
      AccessControlData: new AccessControlFormData(),
 | 
			
		||||
      CpuLimit: 0,
 | 
			
		||||
      MemoryLimit: 0,
 | 
			
		||||
      MemoryReservation: 0,
 | 
			
		||||
      ShmSize: 64,
 | 
			
		||||
      NodeName: null,
 | 
			
		||||
      capabilities: [],
 | 
			
		||||
      Sysctls: [],
 | 
			
		||||
      RegistryModel: new PorImageRegistryModel(),
 | 
			
		||||
      commands: commandsTabUtils.getDefaultViewModel(),
 | 
			
		||||
      envVars: envVarsTabUtils.getDefaultViewModel(),
 | 
			
		||||
      volumes: volumesTabUtils.getDefaultViewModel(),
 | 
			
		||||
      network: networkTabUtils.getDefaultViewModel(),
 | 
			
		||||
      resources: resourcesTabUtils.getDefaultViewModel(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -138,6 +134,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.onResourcesChange = function (resources) {
 | 
			
		||||
      return $scope.$evalAsync(() => {
 | 
			
		||||
        $scope.formValues.resources = resources;
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function onAlwaysPullChange(checked) {
 | 
			
		||||
      return $scope.$evalAsync(() => {
 | 
			
		||||
        $scope.formValues.alwaysPull = checked;
 | 
			
		||||
| 
						 | 
				
			
			@ -299,57 +301,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
      config.Labels = labels;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareDevices(config) {
 | 
			
		||||
      var path = [];
 | 
			
		||||
      config.HostConfig.Devices.forEach(function (p) {
 | 
			
		||||
        if (p.pathOnHost) {
 | 
			
		||||
          if (p.pathInContainer === '') {
 | 
			
		||||
            p.pathInContainer = p.pathOnHost;
 | 
			
		||||
          }
 | 
			
		||||
          path.push({ PathOnHost: p.pathOnHost, PathInContainer: p.pathInContainer, CgroupPermissions: 'rwm' });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      config.HostConfig.Devices = path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareSysctls(config) {
 | 
			
		||||
      var sysctls = {};
 | 
			
		||||
      $scope.formValues.Sysctls.forEach(function (sysctl) {
 | 
			
		||||
        if (sysctl.name && sysctl.value) {
 | 
			
		||||
          sysctls[sysctl.name] = sysctl.value;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      config.HostConfig.Sysctls = sysctls;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareResources(config) {
 | 
			
		||||
      // Shared Memory Size - Round to 0.125
 | 
			
		||||
      if ($scope.formValues.ShmSize >= 0) {
 | 
			
		||||
        var shmSize = (Math.round($scope.formValues.ShmSize * 8) / 8).toFixed(3);
 | 
			
		||||
        shmSize *= 1024 * 1024;
 | 
			
		||||
        config.HostConfig.ShmSize = shmSize;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Memory Limit - Round to 0.125
 | 
			
		||||
      if ($scope.formValues.MemoryLimit >= 0) {
 | 
			
		||||
        var memoryLimit = (Math.round($scope.formValues.MemoryLimit * 8) / 8).toFixed(3);
 | 
			
		||||
        memoryLimit *= 1024 * 1024;
 | 
			
		||||
        config.HostConfig.Memory = memoryLimit;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Memory Resevation - Round to 0.125
 | 
			
		||||
      if ($scope.formValues.MemoryReservation >= 0) {
 | 
			
		||||
        var memoryReservation = (Math.round($scope.formValues.MemoryReservation * 8) / 8).toFixed(3);
 | 
			
		||||
        memoryReservation *= 1024 * 1024;
 | 
			
		||||
        config.HostConfig.MemoryReservation = memoryReservation;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // CPU Limit
 | 
			
		||||
      if ($scope.formValues.CpuLimit >= 0) {
 | 
			
		||||
        config.HostConfig.NanoCpus = $scope.formValues.CpuLimit * 1000000000;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareCapabilities(config) {
 | 
			
		||||
      var allowed = $scope.formValues.capabilities.filter(function (item) {
 | 
			
		||||
        return item.allowed === true;
 | 
			
		||||
| 
						 | 
				
			
			@ -365,51 +316,18 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
      config.HostConfig.CapDrop = notAllowed.map(getCapName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareGPUOptions(config) {
 | 
			
		||||
      const driver = 'nvidia';
 | 
			
		||||
      const gpuOptions = $scope.formValues.GPU;
 | 
			
		||||
      const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, { Driver: driver });
 | 
			
		||||
      if (existingDeviceRequest) {
 | 
			
		||||
        _.pullAllBy(config.HostConfig.DeviceRequests, [existingDeviceRequest], 'Driver');
 | 
			
		||||
      }
 | 
			
		||||
      if (!gpuOptions.enabled) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const deviceRequest = {
 | 
			
		||||
        Driver: driver,
 | 
			
		||||
        Count: -1,
 | 
			
		||||
        DeviceIDs: [], // must be empty if Count != 0 https://github.com/moby/moby/blob/master/daemon/nvidia_linux.go#L50
 | 
			
		||||
        Capabilities: [], // array of ORed arrays of ANDed capabilites = [ [c1 AND c2] OR [c1 AND c3] ] : https://github.com/moby/moby/blob/master/api/types/container/host_config.go#L272
 | 
			
		||||
        // Options: { property1: "string", property2: "string" }, // seems to never be evaluated/used in docker API ?
 | 
			
		||||
      };
 | 
			
		||||
      if (gpuOptions.useSpecific) {
 | 
			
		||||
        deviceRequest.DeviceIDs = gpuOptions.selectedGPUs;
 | 
			
		||||
        deviceRequest.Count = 0;
 | 
			
		||||
      }
 | 
			
		||||
      deviceRequest.Capabilities = [gpuOptions.capabilities];
 | 
			
		||||
 | 
			
		||||
      if (config.HostConfig.DeviceRequests) {
 | 
			
		||||
        config.HostConfig.DeviceRequests.push(deviceRequest);
 | 
			
		||||
      } else {
 | 
			
		||||
        config.HostConfig.DeviceRequests = [deviceRequest];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareConfiguration() {
 | 
			
		||||
      var config = angular.copy($scope.config);
 | 
			
		||||
      config = commandsTabUtils.toRequest(config, $scope.formValues.commands);
 | 
			
		||||
      config = envVarsTabUtils.toRequest(config, $scope.formValues.envVars);
 | 
			
		||||
      config = volumesTabUtils.toRequest(config, $scope.formValues.volumes);
 | 
			
		||||
      config = networkTabUtils.toRequest(config, $scope.formValues.network, $scope.fromContainer.Id);
 | 
			
		||||
      config = resourcesTabUtils.toRequest(config, $scope.formValues.resources);
 | 
			
		||||
 | 
			
		||||
      prepareImageConfig(config);
 | 
			
		||||
      preparePortBindings(config);
 | 
			
		||||
      prepareLabels(config);
 | 
			
		||||
      prepareDevices(config);
 | 
			
		||||
      prepareResources(config);
 | 
			
		||||
      prepareCapabilities(config);
 | 
			
		||||
      prepareSysctls(config);
 | 
			
		||||
      prepareGPUOptions(config);
 | 
			
		||||
      return config;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -426,45 +344,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function loadFromContainerDevices() {
 | 
			
		||||
      var path = [];
 | 
			
		||||
      for (var dev in $scope.config.HostConfig.Devices) {
 | 
			
		||||
        if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) {
 | 
			
		||||
          var device = $scope.config.HostConfig.Devices[dev];
 | 
			
		||||
          path.push({ pathOnHost: device.PathOnHost, pathInContainer: device.PathInContainer });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      $scope.config.HostConfig.Devices = path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function loadFromContainerDeviceRequests() {
 | 
			
		||||
      const deviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
 | 
			
		||||
        return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
 | 
			
		||||
      });
 | 
			
		||||
      if (deviceRequest) {
 | 
			
		||||
        $scope.formValues.GPU.enabled = true;
 | 
			
		||||
        $scope.formValues.GPU.useSpecific = deviceRequest.Count !== -1;
 | 
			
		||||
        $scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs || [];
 | 
			
		||||
        if ($scope.formValues.GPU.useSpecific) {
 | 
			
		||||
          $scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs;
 | 
			
		||||
        } else {
 | 
			
		||||
          $scope.formValues.GPU.selectedGPUs = ['all'];
 | 
			
		||||
        }
 | 
			
		||||
        // we only support a single set of capabilities for now
 | 
			
		||||
        // UI needs to be reworked in order to support OR combinations of AND capabilities
 | 
			
		||||
        $scope.formValues.GPU.capabilities = deviceRequest.Capabilities[0];
 | 
			
		||||
        $scope.formValues.GPU = { ...$scope.formValues.GPU };
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function loadFromContainerSysctls() {
 | 
			
		||||
      for (var s in $scope.config.HostConfig.Sysctls) {
 | 
			
		||||
        if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) {
 | 
			
		||||
          $scope.formValues.Sysctls.push({ name: s, value: $scope.config.HostConfig.Sysctls[s] });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function loadFromContainerImageConfig() {
 | 
			
		||||
      RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image, endpoint.Id)
 | 
			
		||||
        .then((model) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -475,21 +354,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function loadFromContainerResources(d) {
 | 
			
		||||
      if (d.HostConfig.NanoCpus) {
 | 
			
		||||
        $scope.formValues.CpuLimit = d.HostConfig.NanoCpus / 1000000000;
 | 
			
		||||
      }
 | 
			
		||||
      if (d.HostConfig.Memory) {
 | 
			
		||||
        $scope.formValues.MemoryLimit = d.HostConfig.Memory / 1024 / 1024;
 | 
			
		||||
      }
 | 
			
		||||
      if (d.HostConfig.MemoryReservation) {
 | 
			
		||||
        $scope.formValues.MemoryReservation = d.HostConfig.MemoryReservation / 1024 / 1024;
 | 
			
		||||
      }
 | 
			
		||||
      if (d.HostConfig.ShmSize) {
 | 
			
		||||
        $scope.formValues.ShmSize = d.HostConfig.ShmSize / 1024 / 1024;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function loadFromContainerCapabilities(d) {
 | 
			
		||||
      if (d.HostConfig.CapAdd) {
 | 
			
		||||
        d.HostConfig.CapAdd.forEach(function (cap) {
 | 
			
		||||
| 
						 | 
				
			
			@ -543,15 +407,13 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
          $scope.formValues.envVars = envVarsTabUtils.toViewModel(d);
 | 
			
		||||
          $scope.formValues.volumes = volumesTabUtils.toViewModel(d);
 | 
			
		||||
          $scope.formValues.network = networkTabUtils.toViewModel(d, $scope.availableNetworks, $scope.runningContainers);
 | 
			
		||||
          $scope.formValues.resources = resourcesTabUtils.toViewModel(d);
 | 
			
		||||
 | 
			
		||||
          loadFromContainerPortBindings(d);
 | 
			
		||||
          loadFromContainerLabels(d);
 | 
			
		||||
          loadFromContainerDevices(d);
 | 
			
		||||
          loadFromContainerDeviceRequests(d);
 | 
			
		||||
          loadFromContainerImageConfig(d);
 | 
			
		||||
          loadFromContainerResources(d);
 | 
			
		||||
 | 
			
		||||
          loadFromContainerCapabilities(d);
 | 
			
		||||
          loadFromContainerSysctls(d);
 | 
			
		||||
        })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          $scope.state.containerIsLoaded = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -568,7 +430,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
 | 
			
		||||
      $scope.isAdmin = Authentication.isAdmin();
 | 
			
		||||
      $scope.showDeviceMapping = await shouldShowDevices();
 | 
			
		||||
      $scope.showSysctls = await shouldShowSysctls();
 | 
			
		||||
      $scope.allowSysctl = await shouldShowSysctls();
 | 
			
		||||
      $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
 | 
			
		||||
      $scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -647,27 +509,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function updateLimits(config) {
 | 
			
		||||
      try {
 | 
			
		||||
        if ($scope.state.settingUnlimitedResources) {
 | 
			
		||||
          create();
 | 
			
		||||
        } else {
 | 
			
		||||
          await ContainerService.updateLimits($transition$.params().from, config);
 | 
			
		||||
          $scope.config = config;
 | 
			
		||||
          Notifications.success('Success', 'Limits updated');
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        Notifications.error('Failure', err, 'Update Limits fail');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function update() {
 | 
			
		||||
      $scope.state.actionInProgress = true;
 | 
			
		||||
      var config = angular.copy($scope.config);
 | 
			
		||||
      prepareResources(config);
 | 
			
		||||
      await updateLimits(config);
 | 
			
		||||
      $scope.state.actionInProgress = false;
 | 
			
		||||
    }
 | 
			
		||||
    $scope.redeployUnlimitedResources = function (resources) {
 | 
			
		||||
      return $async(async () => {
 | 
			
		||||
        $scope.formValues.resources = resources;
 | 
			
		||||
        return create();
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function create() {
 | 
			
		||||
      var oldContainer = null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -283,233 +283,21 @@
 | 
			
		|||
            <!-- !tab-restart-policy -->
 | 
			
		||||
            <!-- tab-runtime-resources -->
 | 
			
		||||
            <div class="tab-pane" id="runtime-resources">
 | 
			
		||||
              <form class="form-horizontal" style="margin-top: 15px">
 | 
			
		||||
                <div class="col-sm-12 form-section-title"> Runtime </div>
 | 
			
		||||
                <!-- privileged-mode -->
 | 
			
		||||
                <div class="form-group" ng-if="isAdmin || allowPrivilegedMode">
 | 
			
		||||
                  <div class="col-sm-12">
 | 
			
		||||
                    <por-switch-field
 | 
			
		||||
                      label-class="'col-sm-2'"
 | 
			
		||||
                      checked="config.HostConfig.Privileged"
 | 
			
		||||
                      label="'Privileged mode'"
 | 
			
		||||
                      on-change="(handlePrivilegedChange)"
 | 
			
		||||
                    ></por-switch-field>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- !privileged-mode -->
 | 
			
		||||
                <!-- init -->
 | 
			
		||||
                <div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.37">
 | 
			
		||||
                  <div class="col-sm-12">
 | 
			
		||||
                    <por-switch-field label-class="'col-sm-2'" checked="config.HostConfig.Init" label="'Init'" on-change="(handleInitChange)"></por-switch-field>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- !init -->
 | 
			
		||||
                <!-- runtimes -->
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                  <label for="container_runtime" class="col-sm-1 control-label text-left">Runtime</label>
 | 
			
		||||
                  <div class="col-sm-11">
 | 
			
		||||
                    <select class="form-control" ng-model="config.HostConfig.Runtime" id="container_runtime" ng-options="runtime for runtime in availableRuntimes">
 | 
			
		||||
                      <option selected value="">Default</option>
 | 
			
		||||
                    </select>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- !runtimes -->
 | 
			
		||||
              </form>
 | 
			
		||||
              <form class="form-horizontal" style="margin-top: 15px" name="resourceForm">
 | 
			
		||||
                <!-- devices -->
 | 
			
		||||
                <div ng-if="showDeviceMapping" class="form-group">
 | 
			
		||||
                  <div class="col-sm-12" style="margin-top: 5px">
 | 
			
		||||
                    <label class="control-label text-left">Devices</label>
 | 
			
		||||
                    <span class="label label-default interactive" style="margin-left: 10px" ng-click="addDevice()">
 | 
			
		||||
                      <pr-icon icon="'plus'" mode="'alt'"></pr-icon> add device
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- devices-input-list -->
 | 
			
		||||
                  <div class="col-sm-12 form-inline" style="margin-top: 10px">
 | 
			
		||||
                    <div ng-repeat="device in config.HostConfig.Devices" style="margin-top: 2px">
 | 
			
		||||
                      <div class="input-group col-sm-5 input-group-sm">
 | 
			
		||||
                        <span class="input-group-addon">host</span>
 | 
			
		||||
                        <input type="text" class="form-control" ng-model="device.pathOnHost" placeholder="e.g. /dev/tty0" />
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <div class="input-group col-sm-5 input-group-sm">
 | 
			
		||||
                        <span class="input-group-addon">container</span>
 | 
			
		||||
                        <input type="text" class="form-control" ng-model="device.pathInContainer" placeholder="e.g. /dev/tty0" />
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <button class="btn btn-sm btn-light" type="button" ng-click="removeDevice($index)">
 | 
			
		||||
                        <pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
 | 
			
		||||
                      </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- !devices-input-list -->
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- !devices-->
 | 
			
		||||
                <!-- sysctls -->
 | 
			
		||||
                <div ng-if="showSysctls" class="form-group">
 | 
			
		||||
                  <div class="col-sm-12" style="margin-top: 5px">
 | 
			
		||||
                    <label class="control-label text-left">Sysctls</label>
 | 
			
		||||
                    <span class="label label-default interactive" style="margin-left: 10px" ng-click="addSysctl()"> <pr-icon icon="'plus'"></pr-icon> add sysctl </span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- sysctls-input-list -->
 | 
			
		||||
                  <div class="col-sm-12 form-inline" style="margin-top: 10px">
 | 
			
		||||
                    <div ng-repeat="sysctl in formValues.Sysctls" style="margin-top: 2px">
 | 
			
		||||
                      <div class="input-group col-sm-5 input-group-sm">
 | 
			
		||||
                        <span class="input-group-addon">name</span>
 | 
			
		||||
                        <input type="text" class="form-control" ng-model="sysctl.name" placeholder="e.g. FOO" />
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <div class="input-group col-sm-5 input-group-sm">
 | 
			
		||||
                        <span class="input-group-addon">value</span>
 | 
			
		||||
                        <input type="text" class="form-control" ng-model="sysctl.value" placeholder="e.g. bar" />
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <button class="btn btn-sm btn-light" type="button" ng-click="removeSysctl($index)">
 | 
			
		||||
                        <pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
 | 
			
		||||
                      </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- !sysctls-input-list -->
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- !sysctls -->
 | 
			
		||||
                <!-- shm-size-input -->
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                  <label for="shm-size" class="col-sm-2 control-label text-left"> Shared memory size </label>
 | 
			
		||||
                  <div class="col-sm-2">
 | 
			
		||||
                    <input type="number" min="1" class="form-control" ng-model="formValues.ShmSize" id="shm-size" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-sm-2">
 | 
			
		||||
                    <p class="small text-muted mt-2"> Size of /dev/shm (<b>MB</b>) </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- !shm-size-input -->
 | 
			
		||||
                <!-- #region GPU -->
 | 
			
		||||
                <div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
 | 
			
		||||
                  <div class="col-sm-12 form-section-title"> GPU </div>
 | 
			
		||||
                  <gpu
 | 
			
		||||
                    ng-if="applicationState.endpoint.apiVersion >= 1.4"
 | 
			
		||||
                    values="formValues.GPU"
 | 
			
		||||
                    on-change="(onGpuChange)"
 | 
			
		||||
                    gpus="endpoint.Gpus"
 | 
			
		||||
                    used-gpus="gpuUseList"
 | 
			
		||||
                    used-all-gpus="gpuUseAll"
 | 
			
		||||
                    enable-gpu-management="endpoint.EnableGPUManagement"
 | 
			
		||||
                  >
 | 
			
		||||
                  </gpu>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- #endregion GPU -->
 | 
			
		||||
                <div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
 | 
			
		||||
                  <div class="col-sm-12 form-section-title"> Resources </div>
 | 
			
		||||
                  <!-- memory-reservation-input -->
 | 
			
		||||
                  <div class="form-group flex">
 | 
			
		||||
                    <label for="memory-reservation" class="col-sm-3 col-lg-2 control-label vertical-center text-left"> Memory reservation (MB) </label>
 | 
			
		||||
                    <div class="col-sm-6">
 | 
			
		||||
                      <slider
 | 
			
		||||
                        on-change="(handleResourceChange)"
 | 
			
		||||
                        model="formValues.MemoryReservation"
 | 
			
		||||
                        floor="0"
 | 
			
		||||
                        ceil="state.sliderMaxMemory"
 | 
			
		||||
                        step="256"
 | 
			
		||||
                        ng-if="state.sliderMaxMemory"
 | 
			
		||||
                      ></slider>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-sm-2 vertical-center">
 | 
			
		||||
                      <input
 | 
			
		||||
                        name="memory_reservation"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                        min="0"
 | 
			
		||||
                        max="{{ state.sliderMaxMemory }}"
 | 
			
		||||
                        class="form-control"
 | 
			
		||||
                        ng-model="formValues.MemoryReservation"
 | 
			
		||||
                        id="memory-reservation"
 | 
			
		||||
                        required
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="form-group" ng-show="resourceForm.memory_reservation.$invalid">
 | 
			
		||||
                    <div class="col-sm-3 col-lg-2"></div>
 | 
			
		||||
                    <div class="col-sm-8 small text-muted">
 | 
			
		||||
                      <div ng-messages="resourceForm.memory-reservation.$error">
 | 
			
		||||
                        <p class="vertical-center text-warning">
 | 
			
		||||
                          <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Value must be between 0 and {{ state.sliderMaxMemory }}.
 | 
			
		||||
                        </p>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- !memory-reservation-input -->
 | 
			
		||||
                  <!-- memory-limit-input -->
 | 
			
		||||
                  <div class="form-group flex">
 | 
			
		||||
                    <label for="memory-limit" class="col-sm-3 col-lg-2 control-label vertical-center text-left"> Memory limit (MB) </label>
 | 
			
		||||
                    <div class="col-sm-6">
 | 
			
		||||
                      <slider
 | 
			
		||||
                        on-change="(handleResourceChange)"
 | 
			
		||||
                        model="formValues.MemoryLimit"
 | 
			
		||||
                        floor="0"
 | 
			
		||||
                        ceil="state.sliderMaxMemory"
 | 
			
		||||
                        step="256"
 | 
			
		||||
                        ng-if="state.sliderMaxMemory"
 | 
			
		||||
                      ></slider>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-sm-2 vertical-center">
 | 
			
		||||
                      <input
 | 
			
		||||
                        name="memory_Limit"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                        min="0"
 | 
			
		||||
                        max="{{ state.sliderMaxMemory }}"
 | 
			
		||||
                        class="form-control"
 | 
			
		||||
                        ng-model="formValues.MemoryLimit"
 | 
			
		||||
                        id="memory-limit"
 | 
			
		||||
                        required
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="form-group" ng-show="resourceForm.memory_Limit.$invalid">
 | 
			
		||||
                    <div class="col-sm-3 col-lg-2"></div>
 | 
			
		||||
                    <div class="col-sm-8 small text-muted">
 | 
			
		||||
                      <div ng-messages="resourceForm.memory-limit.$error">
 | 
			
		||||
                        <p class="vertical-center text-warning">
 | 
			
		||||
                          <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Value must be between 0 and {{ state.sliderMaxMemory }}.
 | 
			
		||||
                        </p>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- !memory-limit-input -->
 | 
			
		||||
                  <!-- cpu-limit-input -->
 | 
			
		||||
                  <div class="form-group flex">
 | 
			
		||||
                    <label for="cpu-limit" class="col-sm-3 col-lg-2 control-label vertical-center text-left"> Maximum CPU usage </label>
 | 
			
		||||
                    <div class="col-sm-8">
 | 
			
		||||
                      <slider
 | 
			
		||||
                        on-change="(handleResourceChange)"
 | 
			
		||||
                        model="formValues.CpuLimit"
 | 
			
		||||
                        floor="0"
 | 
			
		||||
                        ceil="state.sliderMaxCpu"
 | 
			
		||||
                        step="0.1"
 | 
			
		||||
                        precision="2"
 | 
			
		||||
                        ng-if="state.sliderMaxCpu"
 | 
			
		||||
                      ></slider>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- !cpu-limit-input -->
 | 
			
		||||
                  <!-- update-limit-btn -->
 | 
			
		||||
                  <div class="form-group" ng-if="state.mode == 'duplicate'">
 | 
			
		||||
                    <div class="col-sm-12">
 | 
			
		||||
                      <button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        class="btn btn-primary btn-sm"
 | 
			
		||||
                        ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)"
 | 
			
		||||
                        ng-click="update()"
 | 
			
		||||
                        button-spinner="state.actionInProgress"
 | 
			
		||||
                      >
 | 
			
		||||
                        <span ng-hide="state.actionInProgress">Update Limits</span>
 | 
			
		||||
                        <span ng-show="state.actionInProgress">Update in progress...</span>
 | 
			
		||||
                      </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-sm-12" ng-if="state.settingUnlimitedResources">
 | 
			
		||||
                      <p class="text-muted mr-4">
 | 
			
		||||
                        <pr-icon icon="'alert-triangle'" mode="'warning'" class-name="'mt-10'"></pr-icon>
 | 
			
		||||
                        Updating any resource value to ‘unlimited' will redeploy this container.
 | 
			
		||||
                      </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <!-- !update-limit-btn -->
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
              <docker-create-container-resources-tab
 | 
			
		||||
                values="formValues.resources"
 | 
			
		||||
                on-change="(onResourcesChange)"
 | 
			
		||||
                allow-privileged-mode="allowPrivilegedMode"
 | 
			
		||||
                is-devices-field-visible="showDeviceMapping"
 | 
			
		||||
                is-sysctl-field-visible="allowSysctl"
 | 
			
		||||
                is-init-field-visible="applicationState.endpoint.apiVersion >= 1.37"
 | 
			
		||||
                is-image-invalid="!formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)"
 | 
			
		||||
                redeploy="(redeployUnlimitedResources)"
 | 
			
		||||
                is-duplicate="state.mode == 'duplicate'"
 | 
			
		||||
                validation-data="{
 | 
			
		||||
                  maxMemory: state.sliderMaxMemory,
 | 
			
		||||
                  maxCpu: state.sliderMaxCpu,
 | 
			
		||||
                }"
 | 
			
		||||
              ></docker-create-container-resources-tab>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- !tab-runtime-resources -->
 | 
			
		||||
            <!-- tab-container-capabilities -->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,25 +0,0 @@
 | 
			
		|||
.items > * + * {
 | 
			
		||||
  margin-top: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.label {
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  font-size: 0.9em;
 | 
			
		||||
  padding-top: 7px;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item-line {
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item-line.has-error {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.default-item {
 | 
			
		||||
  width: 100% !important;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
import { ComponentType } from 'react';
 | 
			
		||||
import clsx from 'clsx';
 | 
			
		||||
import { FormikErrors } from 'formik';
 | 
			
		||||
import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +9,6 @@ import { TextTip } from '@@/Tip/TextTip';
 | 
			
		|||
import { Input } from '../Input';
 | 
			
		||||
import { FormError } from '../FormError';
 | 
			
		||||
 | 
			
		||||
import styles from './InputList.module.css';
 | 
			
		||||
import { arrayMove } from './utils';
 | 
			
		||||
 | 
			
		||||
type ArrElement<ArrType> = ArrType extends readonly (infer ElementType)[]
 | 
			
		||||
| 
						 | 
				
			
			@ -94,12 +92,9 @@ export function InputList<T = DefaultType>({
 | 
			
		|||
}: Props<T>) {
 | 
			
		||||
  const isAddButtonVisible = !(isAddButtonHidden || readOnly);
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={clsx('form-group', styles.root)}
 | 
			
		||||
      aria-label={ariaLabel || label}
 | 
			
		||||
    >
 | 
			
		||||
    <div className="form-group" aria-label={ariaLabel || label}>
 | 
			
		||||
      {label && (
 | 
			
		||||
        <div className={clsx('col-sm-12', styles.header)}>
 | 
			
		||||
        <div className="col-sm-12">
 | 
			
		||||
          <span className="control-label space-right pt-2 text-left !font-bold">
 | 
			
		||||
            {label}
 | 
			
		||||
            {tooltip && <Tooltip message={tooltip} />}
 | 
			
		||||
| 
						 | 
				
			
			@ -121,14 +116,7 @@ export function InputList<T = DefaultType>({
 | 
			
		|||
              typeof errors === 'object' ? errors[index] : undefined;
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
              <div
 | 
			
		||||
                key={key}
 | 
			
		||||
                className={clsx(
 | 
			
		||||
                  styles.itemLine,
 | 
			
		||||
                  { [styles.hasError]: !!error },
 | 
			
		||||
                  'vertical-center'
 | 
			
		||||
                )}
 | 
			
		||||
              >
 | 
			
		||||
              <div key={key} className="flex">
 | 
			
		||||
                {Item ? (
 | 
			
		||||
                  <Item
 | 
			
		||||
                    item={item}
 | 
			
		||||
| 
						 | 
				
			
			@ -183,7 +171,7 @@ export function InputList<T = DefaultType>({
 | 
			
		|||
      )}
 | 
			
		||||
 | 
			
		||||
      {isAddButtonVisible && (
 | 
			
		||||
        <div className="col-sm-12 mt-5">
 | 
			
		||||
        <div className="col-sm-12 mt-3">
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={handleAdd}
 | 
			
		||||
            disabled={disabled}
 | 
			
		||||
| 
						 | 
				
			
			@ -272,7 +260,7 @@ function DefaultItem({
 | 
			
		|||
      <Input
 | 
			
		||||
        value={item.value}
 | 
			
		||||
        onChange={(e) => onChange({ value: e.target.value })}
 | 
			
		||||
        className={styles.defaultItem}
 | 
			
		||||
        className="!w-full"
 | 
			
		||||
        disabled={disabled}
 | 
			
		||||
        readOnly={readOnly}
 | 
			
		||||
      />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,11 @@
 | 
			
		|||
import { useCallback } from 'react';
 | 
			
		||||
import { ReactElement } from 'react';
 | 
			
		||||
import RcSlider from 'rc-slider';
 | 
			
		||||
import { HandleProps } from 'rc-slider/lib/Handles/Handle';
 | 
			
		||||
 | 
			
		||||
import { SliderTooltip } from '@@/Tip/SliderTooltip';
 | 
			
		||||
 | 
			
		||||
import styles from './Slider.module.css';
 | 
			
		||||
 | 
			
		||||
import 'rc-slider/assets/index.css';
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
| 
						 | 
				
			
			@ -12,8 +14,8 @@ export interface Props {
 | 
			
		|||
  step: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  onChange: (value: number | number[]) => void;
 | 
			
		||||
  dataCy?: string;
 | 
			
		||||
  // true if you want to always show the tooltip
 | 
			
		||||
  dataCy: string;
 | 
			
		||||
  visibleTooltip?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -31,17 +33,6 @@ export function Slider({
 | 
			
		|||
    [max]: visible && value / max > 0.9 ? '' : max.toString(),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const sliderTooltip = useCallback(
 | 
			
		||||
    (node, handleProps) => (
 | 
			
		||||
      <SliderTooltip
 | 
			
		||||
        value={translateMinValue(handleProps.value)}
 | 
			
		||||
        child={node}
 | 
			
		||||
        delay={0}
 | 
			
		||||
      />
 | 
			
		||||
    ),
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.root}>
 | 
			
		||||
      <RcSlider
 | 
			
		||||
| 
						 | 
				
			
			@ -64,3 +55,16 @@ function translateMinValue(value: number) {
 | 
			
		|||
  }
 | 
			
		||||
  return value.toString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sliderTooltip(
 | 
			
		||||
  node: ReactElement<HandleProps>,
 | 
			
		||||
  handleProps: { value: number }
 | 
			
		||||
) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SliderTooltip
 | 
			
		||||
      value={translateMinValue(handleProps.value)}
 | 
			
		||||
      child={node}
 | 
			
		||||
      delay={0}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
import { Input } from '../Input';
 | 
			
		||||
 | 
			
		||||
import { Slider } from './Slider';
 | 
			
		||||
 | 
			
		||||
export function SliderWithInput({
 | 
			
		||||
  value,
 | 
			
		||||
  onChange,
 | 
			
		||||
  max,
 | 
			
		||||
}: {
 | 
			
		||||
  value: number;
 | 
			
		||||
  onChange: (value: number) => void;
 | 
			
		||||
  max: number;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex items-center gap-4">
 | 
			
		||||
      {max && (
 | 
			
		||||
        <div className="mr-2 flex-1">
 | 
			
		||||
          <Slider
 | 
			
		||||
            onChange={(value) =>
 | 
			
		||||
              onChange(typeof value === 'number' ? value : value[0])
 | 
			
		||||
            }
 | 
			
		||||
            value={value}
 | 
			
		||||
            min={0}
 | 
			
		||||
            max={max}
 | 
			
		||||
            step={256}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      <Input
 | 
			
		||||
        type="number"
 | 
			
		||||
        min="0"
 | 
			
		||||
        max={max}
 | 
			
		||||
        value={value}
 | 
			
		||||
        onChange={(e) => onChange(e.target.valueAsNumber)}
 | 
			
		||||
        className="w-32"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,86 @@
 | 
			
		|||
import { FormikErrors } from 'formik';
 | 
			
		||||
import { array, object, SchemaOf, string } from 'yup';
 | 
			
		||||
import { DeviceMapping } from 'docker-types/generated/1.41';
 | 
			
		||||
 | 
			
		||||
import { FormError } from '@@/form-components/FormError';
 | 
			
		||||
import { InputList, ItemProps } from '@@/form-components/InputList';
 | 
			
		||||
import { InputLabeled } from '@@/form-components/Input/InputLabeled';
 | 
			
		||||
 | 
			
		||||
interface Device {
 | 
			
		||||
  pathOnHost: string;
 | 
			
		||||
  pathInContainer: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Values = Array<Device>;
 | 
			
		||||
 | 
			
		||||
export function DevicesField({
 | 
			
		||||
  values,
 | 
			
		||||
  onChange,
 | 
			
		||||
  errors,
 | 
			
		||||
}: {
 | 
			
		||||
  values: Values;
 | 
			
		||||
  onChange: (value: Values) => void;
 | 
			
		||||
  errors?: FormikErrors<Device>[];
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <InputList
 | 
			
		||||
      value={values}
 | 
			
		||||
      onChange={onChange}
 | 
			
		||||
      item={Item}
 | 
			
		||||
      addLabel="Add device"
 | 
			
		||||
      label="Devices"
 | 
			
		||||
      errors={errors}
 | 
			
		||||
      itemBuilder={() => ({ pathOnHost: '', pathInContainer: '' })}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Item({ item, onChange, error }: ItemProps<Device>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full">
 | 
			
		||||
      <div className="flex w-full gap-4">
 | 
			
		||||
        <InputLabeled
 | 
			
		||||
          value={item.pathOnHost}
 | 
			
		||||
          onChange={(e) => onChange({ ...item, pathOnHost: e.target.value })}
 | 
			
		||||
          label="host"
 | 
			
		||||
          placeholder="e.g. /dev/tty0"
 | 
			
		||||
          className="w-1/2"
 | 
			
		||||
          size="small"
 | 
			
		||||
        />
 | 
			
		||||
        <InputLabeled
 | 
			
		||||
          value={item.pathInContainer}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            onChange({ ...item, pathInContainer: e.target.value })
 | 
			
		||||
          }
 | 
			
		||||
          label="container"
 | 
			
		||||
          placeholder="e.g. /dev/tty0"
 | 
			
		||||
          className="w-1/2"
 | 
			
		||||
          size="small"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      {error && <FormError>{Object.values(error)[0]}</FormError>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function devicesValidation(): SchemaOf<Values> {
 | 
			
		||||
  return array(
 | 
			
		||||
    object({
 | 
			
		||||
      pathOnHost: string().required('Host path is required'),
 | 
			
		||||
      pathInContainer: string().required('Container path is required'),
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toDevicesViewModel(devices: Array<DeviceMapping>): Values {
 | 
			
		||||
  return devices.filter(hasPath).map((device) => ({
 | 
			
		||||
    pathOnHost: device.PathOnHost,
 | 
			
		||||
    pathInContainer: device.PathInContainer,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  function hasPath(
 | 
			
		||||
    device: DeviceMapping
 | 
			
		||||
  ): device is { PathOnHost: string; PathInContainer: string } {
 | 
			
		||||
    return !!device.PathOnHost && !!device.PathInContainer;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,123 @@
 | 
			
		|||
import { Formik } from 'formik';
 | 
			
		||||
import { useMutation } from 'react-query';
 | 
			
		||||
import { useCurrentStateAndParams } from '@uirouter/react';
 | 
			
		||||
 | 
			
		||||
import { notifySuccess } from '@/portainer/services/notifications';
 | 
			
		||||
import { mutationOptions, withError } from '@/react-tools/react-query';
 | 
			
		||||
import { useSystemLimits } from '@/react/docker/proxy/queries/useInfo';
 | 
			
		||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
 | 
			
		||||
import { EnvironmentId } from '@/react/portainer/environments/types';
 | 
			
		||||
 | 
			
		||||
import { LoadingButton } from '@@/buttons';
 | 
			
		||||
import { TextTip } from '@@/Tip/TextTip';
 | 
			
		||||
 | 
			
		||||
import { updateContainer } from '../../queries/useUpdateContainer';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ResourceFieldset,
 | 
			
		||||
  resourcesValidation,
 | 
			
		||||
  Values,
 | 
			
		||||
} from './ResourcesFieldset';
 | 
			
		||||
import { toConfigCpu, toConfigMemory } from './memory-utils';
 | 
			
		||||
 | 
			
		||||
export function EditResourcesForm({
 | 
			
		||||
  redeploy,
 | 
			
		||||
  initialValues,
 | 
			
		||||
  isImageInvalid,
 | 
			
		||||
}: {
 | 
			
		||||
  initialValues: Values;
 | 
			
		||||
  redeploy: (values: Values) => Promise<void>;
 | 
			
		||||
  isImageInvalid: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  const {
 | 
			
		||||
    params: { from: containerId },
 | 
			
		||||
  } = useCurrentStateAndParams();
 | 
			
		||||
  if (!containerId || typeof containerId !== 'string') {
 | 
			
		||||
    throw new Error('missing parameter "from"');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const updateMutation = useMutation(
 | 
			
		||||
    updateLimitsOrCreate,
 | 
			
		||||
    mutationOptions(withError('Failed to update limits'))
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const environmentId = useEnvironmentId();
 | 
			
		||||
  const systemLimits = useSystemLimits(environmentId);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Formik
 | 
			
		||||
      initialValues={initialValues}
 | 
			
		||||
      onSubmit={handleSubmit}
 | 
			
		||||
      validationSchema={() => resourcesValidation(systemLimits)}
 | 
			
		||||
    >
 | 
			
		||||
      {({ values, errors, setValues, dirty, submitForm }) => (
 | 
			
		||||
        <div className="edit-resources p-5">
 | 
			
		||||
          <ResourceFieldset
 | 
			
		||||
            values={values}
 | 
			
		||||
            onChange={setValues}
 | 
			
		||||
            errors={errors}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <div className="form-group">
 | 
			
		||||
            <div className="col-sm-12 flex items-center gap-4">
 | 
			
		||||
              <LoadingButton
 | 
			
		||||
                isLoading={updateMutation.isLoading}
 | 
			
		||||
                disabled={isImageInvalid || !dirty}
 | 
			
		||||
                loadingText="Update in progress..."
 | 
			
		||||
                type="button"
 | 
			
		||||
                onClick={submitForm}
 | 
			
		||||
              >
 | 
			
		||||
                Update Limits
 | 
			
		||||
              </LoadingButton>
 | 
			
		||||
              {settingUnlimitedResources(values) && (
 | 
			
		||||
                <TextTip>
 | 
			
		||||
                  Updating any resource value to 'unlimited' will
 | 
			
		||||
                  redeploy this container.
 | 
			
		||||
                </TextTip>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </Formik>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(values: Values) {
 | 
			
		||||
    updateMutation.mutate(values, {
 | 
			
		||||
      onSuccess: () => {
 | 
			
		||||
        notifySuccess('Success', 'Limits updated');
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function settingUnlimitedResources(values: Values) {
 | 
			
		||||
    return (
 | 
			
		||||
      (initialValues.limit > 0 && values.limit === 0) ||
 | 
			
		||||
      (initialValues.reservation > 0 && values.reservation === 0) ||
 | 
			
		||||
      (initialValues.cpu > 0 && values.cpu === 0)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function updateLimitsOrCreate(values: Values) {
 | 
			
		||||
    if (settingUnlimitedResources(values)) {
 | 
			
		||||
      return redeploy(values);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return updateLimits(environmentId, containerId, values);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function updateLimits(
 | 
			
		||||
  environmentId: EnvironmentId,
 | 
			
		||||
  containerId: string,
 | 
			
		||||
  values: Values
 | 
			
		||||
) {
 | 
			
		||||
  return updateContainer(environmentId, containerId, {
 | 
			
		||||
    // MemorySwap: must be set
 | 
			
		||||
    // -1: non limits, 0: treated as unset(cause update error).
 | 
			
		||||
    MemorySwap: -1,
 | 
			
		||||
    MemoryReservation: toConfigMemory(values.reservation),
 | 
			
		||||
    Memory: toConfigMemory(values.limit),
 | 
			
		||||
    NanoCpus: toConfigCpu(values.cpu),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -12,12 +12,7 @@ import { Switch } from '@@/form-components/SwitchField/Switch';
 | 
			
		|||
import { Tooltip } from '@@/Tip/Tooltip';
 | 
			
		||||
import { TextTip } from '@@/Tip/TextTip';
 | 
			
		||||
 | 
			
		||||
interface Values {
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
  useSpecific: boolean;
 | 
			
		||||
  selectedGPUs: string[];
 | 
			
		||||
  capabilities: string[];
 | 
			
		||||
}
 | 
			
		||||
import { Values } from './types';
 | 
			
		||||
 | 
			
		||||
interface GpuOption {
 | 
			
		||||
  value: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +20,7 @@ interface GpuOption {
 | 
			
		|||
  description?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GPU {
 | 
			
		||||
interface GPU {
 | 
			
		||||
  value: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -71,35 +66,9 @@ const NvidiaCapabilitiesOptions = [
 | 
			
		|||
    label: 'display',
 | 
			
		||||
    description: 'required for leveraging X11 display',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
function Option(props: OptionProps<GpuOption, true>) {
 | 
			
		||||
  const {
 | 
			
		||||
    data: { value, description },
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
 | 
			
		||||
      <components.Option {...props}>
 | 
			
		||||
        {`${value} - ${description}`}
 | 
			
		||||
      </components.Option>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MultiValueRemove(props: MultiValueRemoveProps<GpuOption, true>) {
 | 
			
		||||
  const {
 | 
			
		||||
    selectProps: { value },
 | 
			
		||||
  } = props;
 | 
			
		||||
  if (value && (value as MultiValue<GpuOption>).length === 1) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  // eslint-disable-next-line react/jsx-props-no-spreading
 | 
			
		||||
  return <components.MultiValueRemove {...props} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Gpu({
 | 
			
		||||
export function GpuFieldset({
 | 
			
		||||
  values,
 | 
			
		||||
  onChange,
 | 
			
		||||
  gpus = [],
 | 
			
		||||
| 
						 | 
				
			
			@ -124,43 +93,6 @@ export function Gpu({
 | 
			
		|||
    return options;
 | 
			
		||||
  }, [gpus, usedGpus, usedAllGpus]);
 | 
			
		||||
 | 
			
		||||
  function onChangeValues(key: string, newValue: boolean | string[]) {
 | 
			
		||||
    const newValues = {
 | 
			
		||||
      ...values,
 | 
			
		||||
      [key]: newValue,
 | 
			
		||||
    };
 | 
			
		||||
    onChange(newValues);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function toggleEnableGpu() {
 | 
			
		||||
    onChangeValues('enabled', !values.enabled);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onChangeSelectedGpus(
 | 
			
		||||
    newValue: OnChangeValue<GpuOption, true>,
 | 
			
		||||
    actionMeta: ActionMeta<GpuOption>
 | 
			
		||||
  ) {
 | 
			
		||||
    let { useSpecific } = values;
 | 
			
		||||
    let selectedGPUs = newValue.map((option) => option.value);
 | 
			
		||||
 | 
			
		||||
    if (actionMeta.action === 'select-option') {
 | 
			
		||||
      useSpecific = actionMeta.option?.value !== 'all';
 | 
			
		||||
      selectedGPUs = selectedGPUs.filter((value) =>
 | 
			
		||||
        useSpecific ? value !== 'all' : value === 'all'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newValues = { ...values, selectedGPUs, useSpecific };
 | 
			
		||||
    onChange(newValues);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onChangeSelectedCaps(newValue: OnChangeValue<GpuOption, true>) {
 | 
			
		||||
    onChangeValues(
 | 
			
		||||
      'capabilities',
 | 
			
		||||
      newValue.map((option) => option.value)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const gpuCmd = useMemo(() => {
 | 
			
		||||
    const devices = values.selectedGPUs.join(',');
 | 
			
		||||
    const deviceStr = devices === 'all' ? 'all,' : `device=${devices},`;
 | 
			
		||||
| 
						 | 
				
			
			@ -250,4 +182,67 @@ export function Gpu({
 | 
			
		|||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function onChangeValues(key: string, newValue: boolean | string[]) {
 | 
			
		||||
    const newValues = {
 | 
			
		||||
      ...values,
 | 
			
		||||
      [key]: newValue,
 | 
			
		||||
    };
 | 
			
		||||
    onChange(newValues);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function toggleEnableGpu() {
 | 
			
		||||
    onChangeValues('enabled', !values.enabled);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onChangeSelectedGpus(
 | 
			
		||||
    newValue: OnChangeValue<GpuOption, true>,
 | 
			
		||||
    actionMeta: ActionMeta<GpuOption>
 | 
			
		||||
  ) {
 | 
			
		||||
    let { useSpecific } = values;
 | 
			
		||||
    let selectedGPUs = newValue.map((option) => option.value);
 | 
			
		||||
 | 
			
		||||
    if (actionMeta.action === 'select-option') {
 | 
			
		||||
      useSpecific = actionMeta.option?.value !== 'all';
 | 
			
		||||
      selectedGPUs = selectedGPUs.filter((value) =>
 | 
			
		||||
        useSpecific ? value !== 'all' : value === 'all'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newValues = { ...values, selectedGPUs, useSpecific };
 | 
			
		||||
    onChange(newValues);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onChangeSelectedCaps(newValue: OnChangeValue<GpuOption, true>) {
 | 
			
		||||
    onChangeValues(
 | 
			
		||||
      'capabilities',
 | 
			
		||||
      newValue.map((option) => option.value)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Option(props: OptionProps<GpuOption, true>) {
 | 
			
		||||
  const {
 | 
			
		||||
    data: { value, description },
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
 | 
			
		||||
      <components.Option {...props}>
 | 
			
		||||
        {`${value} - ${description}`}
 | 
			
		||||
      </components.Option>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MultiValueRemove(props: MultiValueRemoveProps<GpuOption, true>) {
 | 
			
		||||
  const {
 | 
			
		||||
    selectProps: { value },
 | 
			
		||||
  } = props;
 | 
			
		||||
  if (value && (value as MultiValue<GpuOption>).length === 1) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  // eslint-disable-next-line react/jsx-props-no-spreading
 | 
			
		||||
  return <components.MultiValueRemove {...props} />;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
import { validation } from './validation';
 | 
			
		||||
import { toRequest } from './toRequest';
 | 
			
		||||
import { toViewModel, getDefaultViewModel } from './toViewModel';
 | 
			
		||||
 | 
			
		||||
export { GpuFieldset } from './GpuFieldset';
 | 
			
		||||
 | 
			
		||||
export type { Values as GpuFieldsetValues } from './types';
 | 
			
		||||
 | 
			
		||||
export const gpuFieldsetUtils = {
 | 
			
		||||
  toRequest,
 | 
			
		||||
  toViewModel,
 | 
			
		||||
  validation,
 | 
			
		||||
  getDefaultViewModel,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import { DeviceRequest } from 'docker-types/generated/1.41';
 | 
			
		||||
 | 
			
		||||
import { Values } from './types';
 | 
			
		||||
 | 
			
		||||
export function toRequest(
 | 
			
		||||
  deviceRequests: Array<DeviceRequest>,
 | 
			
		||||
  gpu: Values
 | 
			
		||||
): Array<DeviceRequest> {
 | 
			
		||||
  const driver = 'nvidia';
 | 
			
		||||
 | 
			
		||||
  const otherDeviceRequests = deviceRequests.filter(
 | 
			
		||||
    (deviceRequest) => deviceRequest.Driver !== driver
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (!gpu.enabled) {
 | 
			
		||||
    return otherDeviceRequests;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const deviceRequest: DeviceRequest = {
 | 
			
		||||
    Driver: driver,
 | 
			
		||||
    Count: -1,
 | 
			
		||||
    DeviceIDs: [], // must be empty if Count != 0 https://github.com/moby/moby/blob/master/daemon/nvidia_linux.go#L50
 | 
			
		||||
    Capabilities: [], // array of ORed arrays of ANDed capabilites = [ [c1 AND c2] OR [c1 AND c3] ] : https://github.com/moby/moby/blob/master/api/types/container/host_config.go#L272
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (gpu.useSpecific) {
 | 
			
		||||
    deviceRequest.DeviceIDs = gpu.selectedGPUs;
 | 
			
		||||
    deviceRequest.Count = 0;
 | 
			
		||||
  }
 | 
			
		||||
  deviceRequest.Capabilities = [gpu.capabilities];
 | 
			
		||||
 | 
			
		||||
  return [...otherDeviceRequests, deviceRequest];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
import { DeviceRequest } from 'docker-types/generated/1.41';
 | 
			
		||||
 | 
			
		||||
import { Values } from './types';
 | 
			
		||||
 | 
			
		||||
export function getDefaultViewModel(): Values {
 | 
			
		||||
  return {
 | 
			
		||||
    enabled: false,
 | 
			
		||||
    useSpecific: false,
 | 
			
		||||
    selectedGPUs: ['all'],
 | 
			
		||||
    capabilities: ['compute', 'utility'],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toViewModel(deviceRequests: Array<DeviceRequest> = []): Values {
 | 
			
		||||
  const deviceRequest = deviceRequests.find(
 | 
			
		||||
    (o) => o.Driver === 'nvidia' || o.Capabilities?.[0]?.[0] === 'gpu'
 | 
			
		||||
  );
 | 
			
		||||
  if (!deviceRequest) {
 | 
			
		||||
    return getDefaultViewModel();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const useSpecific = deviceRequest.Count !== -1;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    enabled: true,
 | 
			
		||||
    useSpecific,
 | 
			
		||||
    selectedGPUs: useSpecific ? deviceRequest.DeviceIDs || [] : ['all'],
 | 
			
		||||
    capabilities: deviceRequest.Capabilities?.[0] || [],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
export interface Values {
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
  useSpecific: boolean;
 | 
			
		||||
  selectedGPUs: string[];
 | 
			
		||||
  capabilities: string[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
import { SchemaOf, object, array, string, bool } from 'yup';
 | 
			
		||||
 | 
			
		||||
import { Values } from './types';
 | 
			
		||||
 | 
			
		||||
export function validation(): SchemaOf<Values> {
 | 
			
		||||
  return object({
 | 
			
		||||
    capabilities: array()
 | 
			
		||||
      .of(string().default(''))
 | 
			
		||||
      .default(['compute', 'utility']),
 | 
			
		||||
    enabled: bool().default(false),
 | 
			
		||||
    selectedGPUs: array().of(string()).default(['all']),
 | 
			
		||||
    useSpecific: bool().default(false),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,103 @@
 | 
			
		|||
import { FormikErrors } from 'formik';
 | 
			
		||||
import { number, object, SchemaOf } from 'yup';
 | 
			
		||||
 | 
			
		||||
import { useSystemLimits } from '@/react/docker/proxy/queries/useInfo';
 | 
			
		||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
 | 
			
		||||
 | 
			
		||||
import { FormControl } from '@@/form-components/FormControl';
 | 
			
		||||
import { FormSection } from '@@/form-components/FormSection';
 | 
			
		||||
import { Slider } from '@@/form-components/Slider';
 | 
			
		||||
import { SliderWithInput } from '@@/form-components/Slider/SliderWithInput';
 | 
			
		||||
 | 
			
		||||
import { CreateContainerRequest } from '../types';
 | 
			
		||||
 | 
			
		||||
import { toConfigCpu, toConfigMemory } from './memory-utils';
 | 
			
		||||
 | 
			
		||||
export interface Values {
 | 
			
		||||
  reservation: number;
 | 
			
		||||
  limit: number;
 | 
			
		||||
  cpu: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ResourceFieldset({
 | 
			
		||||
  values,
 | 
			
		||||
  onChange,
 | 
			
		||||
  errors,
 | 
			
		||||
}: {
 | 
			
		||||
  values: Values;
 | 
			
		||||
  onChange: (values: Values) => void;
 | 
			
		||||
  errors: FormikErrors<Values> | undefined;
 | 
			
		||||
}) {
 | 
			
		||||
  const environmentId = useEnvironmentId();
 | 
			
		||||
  const { maxCpu, maxMemory } = useSystemLimits(environmentId);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FormSection title="Resources">
 | 
			
		||||
      <FormControl label="Memory reservation (MB)" errors={errors?.reservation}>
 | 
			
		||||
        <SliderWithInput
 | 
			
		||||
          value={values.reservation}
 | 
			
		||||
          onChange={(value) => onChange({ ...values, reservation: value })}
 | 
			
		||||
          max={maxMemory}
 | 
			
		||||
        />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
 | 
			
		||||
      <FormControl label="Memory limit (MB)" errors={errors?.limit}>
 | 
			
		||||
        <SliderWithInput
 | 
			
		||||
          value={values.limit}
 | 
			
		||||
          onChange={(value) => onChange({ ...values, limit: value })}
 | 
			
		||||
          max={maxMemory}
 | 
			
		||||
        />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
 | 
			
		||||
      <FormControl label="Maximum CPU usage" errors={errors?.cpu}>
 | 
			
		||||
        <Slider
 | 
			
		||||
          value={values.cpu}
 | 
			
		||||
          onChange={(value) =>
 | 
			
		||||
            onChange({
 | 
			
		||||
              ...values,
 | 
			
		||||
              cpu: typeof value === 'number' ? value : value[0],
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          min={0}
 | 
			
		||||
          max={maxCpu}
 | 
			
		||||
          step={0.1}
 | 
			
		||||
        />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
    </FormSection>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toRequest(
 | 
			
		||||
  oldConfig: CreateContainerRequest['HostConfig'],
 | 
			
		||||
  values: Values
 | 
			
		||||
): CreateContainerRequest['HostConfig'] {
 | 
			
		||||
  return {
 | 
			
		||||
    ...oldConfig,
 | 
			
		||||
    NanoCpus: toConfigCpu(values.cpu),
 | 
			
		||||
    MemoryReservation: toConfigMemory(values.reservation),
 | 
			
		||||
    Memory: toConfigMemory(values.limit),
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function resourcesValidation({
 | 
			
		||||
  maxMemory = Number.POSITIVE_INFINITY,
 | 
			
		||||
  maxCpu = Number.POSITIVE_INFINITY,
 | 
			
		||||
}: {
 | 
			
		||||
  maxMemory?: number;
 | 
			
		||||
  maxCpu?: number;
 | 
			
		||||
} = {}): SchemaOf<Values> {
 | 
			
		||||
  return object({
 | 
			
		||||
    reservation: number()
 | 
			
		||||
      .min(0)
 | 
			
		||||
      .max(maxMemory, `Value must be between 0 and ${maxMemory}`)
 | 
			
		||||
      .default(0),
 | 
			
		||||
    limit: number()
 | 
			
		||||
      .min(0)
 | 
			
		||||
      .max(maxMemory, `Value must be between 0 and ${maxMemory}`)
 | 
			
		||||
      .default(0),
 | 
			
		||||
    cpu: number()
 | 
			
		||||
      .min(0)
 | 
			
		||||
      .max(maxCpu, `Value must be between 0 and ${maxCpu}`)
 | 
			
		||||
      .default(0),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,148 @@
 | 
			
		|||
import _ from 'lodash';
 | 
			
		||||
import { FormikErrors } from 'formik';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { useIsStandAlone } from '@/react/docker/proxy/queries/useInfo';
 | 
			
		||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
 | 
			
		||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
 | 
			
		||||
 | 
			
		||||
import { FormControl } from '@@/form-components/FormControl';
 | 
			
		||||
import { Input } from '@@/form-components/Input';
 | 
			
		||||
 | 
			
		||||
import { GpuFieldset, GpuFieldsetValues } from './GpuFieldset';
 | 
			
		||||
import { Values as RuntimeValues, RuntimeSection } from './RuntimeSection';
 | 
			
		||||
import { DevicesField, Values as Devices } from './DevicesField';
 | 
			
		||||
import { SysctlsField, Values as Sysctls } from './SysctlsField';
 | 
			
		||||
import {
 | 
			
		||||
  ResourceFieldset,
 | 
			
		||||
  Values as ResourcesValues,
 | 
			
		||||
} from './ResourcesFieldset';
 | 
			
		||||
import { EditResourcesForm } from './EditResourceForm';
 | 
			
		||||
 | 
			
		||||
export interface Values {
 | 
			
		||||
  runtime: RuntimeValues;
 | 
			
		||||
 | 
			
		||||
  devices: Devices;
 | 
			
		||||
 | 
			
		||||
  sysctls: Sysctls;
 | 
			
		||||
 | 
			
		||||
  sharedMemorySize: number;
 | 
			
		||||
 | 
			
		||||
  gpu: GpuFieldsetValues;
 | 
			
		||||
 | 
			
		||||
  resources: ResourcesValues;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ResourcesTab({
 | 
			
		||||
  values: initialValues,
 | 
			
		||||
  onChange,
 | 
			
		||||
  allowPrivilegedMode,
 | 
			
		||||
  isInitFieldVisible,
 | 
			
		||||
  isDevicesFieldVisible,
 | 
			
		||||
  isSysctlFieldVisible,
 | 
			
		||||
  errors,
 | 
			
		||||
  isDuplicate,
 | 
			
		||||
  redeploy,
 | 
			
		||||
  isImageInvalid,
 | 
			
		||||
}: {
 | 
			
		||||
  values: Values;
 | 
			
		||||
  onChange: (values: Values) => void;
 | 
			
		||||
  allowPrivilegedMode: boolean;
 | 
			
		||||
  isInitFieldVisible: boolean;
 | 
			
		||||
  isDevicesFieldVisible: boolean;
 | 
			
		||||
  isSysctlFieldVisible: boolean;
 | 
			
		||||
  errors?: FormikErrors<Values>;
 | 
			
		||||
  isDuplicate?: boolean;
 | 
			
		||||
  redeploy: (values: Values) => Promise<void>;
 | 
			
		||||
  isImageInvalid: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  const [values, setControlledValues] = useState(initialValues);
 | 
			
		||||
  const environmentId = useEnvironmentId();
 | 
			
		||||
 | 
			
		||||
  const environmentQuery = useCurrentEnvironment();
 | 
			
		||||
 | 
			
		||||
  const isStandalone = useIsStandAlone(environmentId);
 | 
			
		||||
 | 
			
		||||
  if (!environmentQuery.data) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const environment = environmentQuery.data;
 | 
			
		||||
  const gpuUseAll = _.get(environment, 'Snapshots[0].GpuUseAll', false);
 | 
			
		||||
  const gpuUseList = _.get(environment, 'Snapshots[0].GpuUseList', []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="mt-3">
 | 
			
		||||
      <RuntimeSection
 | 
			
		||||
        values={values.runtime}
 | 
			
		||||
        onChange={(runtime) => handleChange({ runtime })}
 | 
			
		||||
        allowPrivilegedMode={allowPrivilegedMode}
 | 
			
		||||
        isInitFieldVisible={isInitFieldVisible}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {isDevicesFieldVisible && (
 | 
			
		||||
        <DevicesField
 | 
			
		||||
          values={values.devices}
 | 
			
		||||
          onChange={(devices) => handleChange({ devices })}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isSysctlFieldVisible && (
 | 
			
		||||
        <SysctlsField
 | 
			
		||||
          values={values.sysctls}
 | 
			
		||||
          onChange={(sysctls) => handleChange({ sysctls })}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <FormControl label="Shared memory size" inputId="shm-size">
 | 
			
		||||
        <div className="flex items-center gap-4">
 | 
			
		||||
          <Input
 | 
			
		||||
            id="shm-size"
 | 
			
		||||
            type="number"
 | 
			
		||||
            min="1"
 | 
			
		||||
            value={values.sharedMemorySize}
 | 
			
		||||
            onChange={(e) =>
 | 
			
		||||
              handleChange({ sharedMemorySize: e.target.valueAsNumber })
 | 
			
		||||
            }
 | 
			
		||||
            className="w-32"
 | 
			
		||||
          />
 | 
			
		||||
          <div className="small text-muted">
 | 
			
		||||
            Size of /dev/shm (<b>MB</b>)
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </FormControl>
 | 
			
		||||
 | 
			
		||||
      {isStandalone && (
 | 
			
		||||
        <GpuFieldset
 | 
			
		||||
          values={values.gpu}
 | 
			
		||||
          onChange={(gpu) => handleChange({ gpu })}
 | 
			
		||||
          gpus={environment.Gpus}
 | 
			
		||||
          enableGpuManagement={environment.EnableGPUManagement}
 | 
			
		||||
          usedGpus={gpuUseList}
 | 
			
		||||
          usedAllGpus={gpuUseAll}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isDuplicate ? (
 | 
			
		||||
        <EditResourcesForm
 | 
			
		||||
          initialValues={values.resources}
 | 
			
		||||
          redeploy={(newValues) =>
 | 
			
		||||
            redeploy({ ...values, resources: newValues })
 | 
			
		||||
          }
 | 
			
		||||
          isImageInvalid={isImageInvalid}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <ResourceFieldset
 | 
			
		||||
          values={values.resources}
 | 
			
		||||
          onChange={(resources) => handleChange({ resources })}
 | 
			
		||||
          errors={errors?.resources}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function handleChange(newValues: Partial<Values>) {
 | 
			
		||||
    onChange({ ...values, ...newValues });
 | 
			
		||||
    setControlledValues({ ...values, ...newValues });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
import { bool, object, SchemaOf, string } from 'yup';
 | 
			
		||||
 | 
			
		||||
import { FormControl } from '@@/form-components/FormControl';
 | 
			
		||||
import { FormSection } from '@@/form-components/FormSection';
 | 
			
		||||
import { SwitchField } from '@@/form-components/SwitchField';
 | 
			
		||||
 | 
			
		||||
import { RuntimeSelector } from './RuntimeSelector';
 | 
			
		||||
 | 
			
		||||
export interface Values {
 | 
			
		||||
  privileged: boolean;
 | 
			
		||||
  init: boolean;
 | 
			
		||||
  type: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function RuntimeSection({
 | 
			
		||||
  values,
 | 
			
		||||
  onChange,
 | 
			
		||||
  allowPrivilegedMode,
 | 
			
		||||
  isInitFieldVisible,
 | 
			
		||||
}: {
 | 
			
		||||
  values: Values;
 | 
			
		||||
  onChange: (values: Values) => void;
 | 
			
		||||
  allowPrivilegedMode: boolean;
 | 
			
		||||
  isInitFieldVisible: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormSection title="Runtime">
 | 
			
		||||
      {allowPrivilegedMode && (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
          <div className="col-sm-12">
 | 
			
		||||
            <SwitchField
 | 
			
		||||
              labelClass="col-sm-2"
 | 
			
		||||
              label="Privileged mode"
 | 
			
		||||
              checked={values.privileged}
 | 
			
		||||
              onChange={(privileged) => handleChange({ privileged })}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isInitFieldVisible && (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
          <div className="col-sm-12">
 | 
			
		||||
            <SwitchField
 | 
			
		||||
              labelClass="col-sm-2"
 | 
			
		||||
              label="Init"
 | 
			
		||||
              checked={values.init}
 | 
			
		||||
              onChange={(init) => handleChange({ init })}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <FormControl label="Type" inputId="container_runtime" size="xsmall">
 | 
			
		||||
        <RuntimeSelector
 | 
			
		||||
          value={values.type}
 | 
			
		||||
          onChange={(type) => handleChange({ type })}
 | 
			
		||||
        />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
    </FormSection>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function handleChange(newValues: Partial<Values>) {
 | 
			
		||||
    onChange({ ...values, ...newValues });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function runtimeValidation(): SchemaOf<Values> {
 | 
			
		||||
  return object({
 | 
			
		||||
    privileged: bool().default(false),
 | 
			
		||||
    init: bool().default(false),
 | 
			
		||||
    type: string().default(''),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
import { useInfo } from '@/react/docker/proxy/queries/useInfo';
 | 
			
		||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
 | 
			
		||||
 | 
			
		||||
import { PortainerSelect } from '@@/form-components/PortainerSelect';
 | 
			
		||||
 | 
			
		||||
export function RuntimeSelector({
 | 
			
		||||
  value,
 | 
			
		||||
  onChange,
 | 
			
		||||
}: {
 | 
			
		||||
  value: string;
 | 
			
		||||
  onChange: (value: string) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  const environmentId = useEnvironmentId();
 | 
			
		||||
  const infoQuery = useInfo(environmentId, (info) => [
 | 
			
		||||
    { label: 'Default', value: '' },
 | 
			
		||||
    ...Object.keys(info?.Runtimes || {}).map((runtime) => ({
 | 
			
		||||
      label: runtime,
 | 
			
		||||
      value: runtime,
 | 
			
		||||
    })),
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <PortainerSelect
 | 
			
		||||
      onChange={onChange}
 | 
			
		||||
      value={value}
 | 
			
		||||
      options={infoQuery.data || []}
 | 
			
		||||
      isLoading={infoQuery.isLoading}
 | 
			
		||||
      disabled={infoQuery.isLoading}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
import { FormikErrors } from 'formik';
 | 
			
		||||
import { array, object, SchemaOf, string } from 'yup';
 | 
			
		||||
 | 
			
		||||
import { FormError } from '@@/form-components/FormError';
 | 
			
		||||
import { InputList, ItemProps } from '@@/form-components/InputList';
 | 
			
		||||
import { InputLabeled } from '@@/form-components/Input/InputLabeled';
 | 
			
		||||
 | 
			
		||||
interface Sysctls {
 | 
			
		||||
  name: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Values = Array<Sysctls>;
 | 
			
		||||
 | 
			
		||||
export function SysctlsField({
 | 
			
		||||
  values,
 | 
			
		||||
  onChange,
 | 
			
		||||
  errors,
 | 
			
		||||
}: {
 | 
			
		||||
  values: Values;
 | 
			
		||||
  onChange: (value: Values) => void;
 | 
			
		||||
  errors?: FormikErrors<Sysctls>[];
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <InputList
 | 
			
		||||
      value={values}
 | 
			
		||||
      onChange={onChange}
 | 
			
		||||
      item={Item}
 | 
			
		||||
      addLabel="Add sysctl"
 | 
			
		||||
      label="Sysctls"
 | 
			
		||||
      errors={errors}
 | 
			
		||||
      itemBuilder={() => ({ name: '', value: '' })}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Item({ item, onChange, error }: ItemProps<Sysctls>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full">
 | 
			
		||||
      <div className="flex w-full gap-4">
 | 
			
		||||
        <InputLabeled
 | 
			
		||||
          value={item.name}
 | 
			
		||||
          onChange={(e) => onChange({ ...item, name: e.target.value })}
 | 
			
		||||
          label="name"
 | 
			
		||||
          placeholder="e.g. FOO"
 | 
			
		||||
          className="w-1/2"
 | 
			
		||||
          size="small"
 | 
			
		||||
        />
 | 
			
		||||
        <InputLabeled
 | 
			
		||||
          value={item.value}
 | 
			
		||||
          onChange={(e) => onChange({ ...item, value: e.target.value })}
 | 
			
		||||
          label="value"
 | 
			
		||||
          placeholder="e.g. bar"
 | 
			
		||||
          className="w-1/2"
 | 
			
		||||
          size="small"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      {error && <FormError>{Object.values(error)[0]}</FormError>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function sysctlsValidation(): SchemaOf<Values> {
 | 
			
		||||
  return array(
 | 
			
		||||
    object({
 | 
			
		||||
      name: string().required('Name is required'),
 | 
			
		||||
      value: string().required('Value is required'),
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
import { validation } from './validation';
 | 
			
		||||
import { toRequest } from './toRequest';
 | 
			
		||||
import { toViewModel, getDefaultViewModel } from './toViewModel';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  ResourcesTab,
 | 
			
		||||
  type Values as ResourcesTabValues,
 | 
			
		||||
} from './ResourcesTab';
 | 
			
		||||
 | 
			
		||||
export const resourcesTabUtils = {
 | 
			
		||||
  toRequest,
 | 
			
		||||
  toViewModel,
 | 
			
		||||
  validation,
 | 
			
		||||
  getDefaultViewModel,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
export function toConfigMemory(value: number): number {
 | 
			
		||||
  if (value < 0) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return round(Math.round(value * 8) / 8, 3) * 1024 * 1024;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toViewModelMemory(value = 0): number {
 | 
			
		||||
  if (value < 0) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return value / 1024 / 1024;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function round(value: number, decimals: number) {
 | 
			
		||||
  const tenth = 10 ** decimals;
 | 
			
		||||
  return Math.round((value + Number.EPSILON) * tenth) / tenth;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toViewModelCpu(value = 0) {
 | 
			
		||||
  if (value < 0) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return value / 1000000000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toConfigCpu(value: number) {
 | 
			
		||||
  if (value < 0) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return value * 1000000000;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
import { CreateContainerRequest } from '../types';
 | 
			
		||||
 | 
			
		||||
import { gpuFieldsetUtils } from './GpuFieldset';
 | 
			
		||||
import { toConfigMemory } from './memory-utils';
 | 
			
		||||
import { Values } from './ResourcesTab';
 | 
			
		||||
import { toRequest as toResourcesRequest } from './ResourcesFieldset';
 | 
			
		||||
 | 
			
		||||
export function toRequest(
 | 
			
		||||
  oldConfig: CreateContainerRequest,
 | 
			
		||||
  values: Values
 | 
			
		||||
): CreateContainerRequest {
 | 
			
		||||
  return {
 | 
			
		||||
    ...oldConfig,
 | 
			
		||||
    HostConfig: {
 | 
			
		||||
      ...oldConfig.HostConfig,
 | 
			
		||||
      Privileged: values.runtime.privileged,
 | 
			
		||||
      Init: values.runtime.init,
 | 
			
		||||
      Runtime: values.runtime.type,
 | 
			
		||||
      Devices: values.devices.map((device) => ({
 | 
			
		||||
        PathOnHost: device.pathOnHost,
 | 
			
		||||
        PathInContainer: device.pathInContainer,
 | 
			
		||||
        CgroupPermissions: 'rwm',
 | 
			
		||||
      })),
 | 
			
		||||
      Sysctls: Object.fromEntries(
 | 
			
		||||
        values.sysctls.map((sysctl) => [sysctl.name, sysctl.value])
 | 
			
		||||
      ),
 | 
			
		||||
      ShmSize: toConfigMemory(values.sharedMemorySize),
 | 
			
		||||
      DeviceRequests: gpuFieldsetUtils.toRequest(
 | 
			
		||||
        oldConfig.HostConfig.DeviceRequests || [],
 | 
			
		||||
        values.gpu
 | 
			
		||||
      ),
 | 
			
		||||
      ...toResourcesRequest(oldConfig.HostConfig, values.resources),
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
import { ContainerJSON } from '../../queries/container';
 | 
			
		||||
 | 
			
		||||
import { toDevicesViewModel } from './DevicesField';
 | 
			
		||||
import { gpuFieldsetUtils } from './GpuFieldset';
 | 
			
		||||
import { toViewModelCpu, toViewModelMemory } from './memory-utils';
 | 
			
		||||
import { Values } from './ResourcesTab';
 | 
			
		||||
 | 
			
		||||
export function toViewModel(config: ContainerJSON): Values {
 | 
			
		||||
  return {
 | 
			
		||||
    runtime: {
 | 
			
		||||
      privileged: config.HostConfig?.Privileged || false,
 | 
			
		||||
      init: config.HostConfig?.Init || false,
 | 
			
		||||
      type: config.HostConfig?.Runtime || '',
 | 
			
		||||
    },
 | 
			
		||||
    devices: toDevicesViewModel(config.HostConfig?.Devices || []),
 | 
			
		||||
    sysctls: Object.entries(config.HostConfig?.Sysctls || {}).map(
 | 
			
		||||
      ([name, value]) => ({
 | 
			
		||||
        name,
 | 
			
		||||
        value,
 | 
			
		||||
      })
 | 
			
		||||
    ),
 | 
			
		||||
    gpu: gpuFieldsetUtils.toViewModel(config.HostConfig?.DeviceRequests || []),
 | 
			
		||||
    sharedMemorySize: toViewModelMemory(config.HostConfig?.ShmSize),
 | 
			
		||||
    resources: {
 | 
			
		||||
      cpu: toViewModelCpu(config.HostConfig?.NanoCpus),
 | 
			
		||||
      reservation: toViewModelMemory(config.HostConfig?.MemoryReservation),
 | 
			
		||||
      limit: toViewModelMemory(config.HostConfig?.Memory),
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getDefaultViewModel(): Values {
 | 
			
		||||
  return {
 | 
			
		||||
    runtime: {
 | 
			
		||||
      privileged: false,
 | 
			
		||||
      init: false,
 | 
			
		||||
      type: '',
 | 
			
		||||
    },
 | 
			
		||||
    devices: [],
 | 
			
		||||
    sysctls: [],
 | 
			
		||||
    sharedMemorySize: 64,
 | 
			
		||||
    gpu: gpuFieldsetUtils.getDefaultViewModel(),
 | 
			
		||||
    resources: {
 | 
			
		||||
      reservation: 0,
 | 
			
		||||
      limit: 0,
 | 
			
		||||
      cpu: 0,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import { number, object, SchemaOf } from 'yup';
 | 
			
		||||
 | 
			
		||||
import { devicesValidation } from './DevicesField';
 | 
			
		||||
import { gpuFieldsetUtils } from './GpuFieldset';
 | 
			
		||||
import { resourcesValidation } from './ResourcesFieldset';
 | 
			
		||||
import { Values } from './ResourcesTab';
 | 
			
		||||
import { runtimeValidation } from './RuntimeSection';
 | 
			
		||||
import { sysctlsValidation } from './SysctlsField';
 | 
			
		||||
 | 
			
		||||
export function validation({
 | 
			
		||||
  maxMemory,
 | 
			
		||||
  maxCpu,
 | 
			
		||||
}: {
 | 
			
		||||
  maxMemory?: number;
 | 
			
		||||
  maxCpu?: number;
 | 
			
		||||
} = {}): SchemaOf<Values> {
 | 
			
		||||
  return object({
 | 
			
		||||
    runtime: runtimeValidation(),
 | 
			
		||||
    devices: devicesValidation(),
 | 
			
		||||
    sysctls: sysctlsValidation(),
 | 
			
		||||
    sharedMemorySize: number().min(0).default(0),
 | 
			
		||||
    gpu: gpuFieldsetUtils.validation(),
 | 
			
		||||
    resources: resourcesValidation({ maxMemory, maxCpu }),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
import { Resources, RestartPolicy } from 'docker-types/generated/1.41';
 | 
			
		||||
import { AxiosRequestHeaders } from 'axios';
 | 
			
		||||
 | 
			
		||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
 | 
			
		||||
import { EnvironmentId } from '@/react/portainer/environments/types';
 | 
			
		||||
 | 
			
		||||
import { urlBuilder } from '../containers.service';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * UpdateConfig holds the mutable attributes of a Container.
 | 
			
		||||
 * Those attributes can be updated at runtime.
 | 
			
		||||
 */
 | 
			
		||||
interface UpdateConfig extends Resources {
 | 
			
		||||
  // Contains container's resources (cgroups, ulimits)
 | 
			
		||||
 | 
			
		||||
  RestartPolicy?: RestartPolicy;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function updateContainer(
 | 
			
		||||
  environmentId: EnvironmentId,
 | 
			
		||||
  containerId: string,
 | 
			
		||||
  config: UpdateConfig,
 | 
			
		||||
  { nodeName }: { nodeName?: string } = {}
 | 
			
		||||
) {
 | 
			
		||||
  const headers: AxiosRequestHeaders = {};
 | 
			
		||||
 | 
			
		||||
  if (nodeName) {
 | 
			
		||||
    headers['X-PortainerAgent-Target'] = nodeName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await axios.post<{ Warnings: string[] }>(
 | 
			
		||||
      urlBuilder(environmentId, containerId, 'update'),
 | 
			
		||||
      config,
 | 
			
		||||
      { headers }
 | 
			
		||||
    );
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    throw parseAxiosError(err, 'failed updating container');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue