From ffac83864dd87b2009a9b69f098cba47d1c987ff Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sun, 24 Sep 2023 15:31:06 +0300 Subject: [PATCH] refactor(containers): migrate resources tab to react [EE-5214] (#10355) --- app/docker/react/components/containers.ts | 21 ++ app/docker/react/components/index.ts | 12 - .../create/createContainerController.js | 189 ++------------ .../containers/create/createcontainer.html | 242 ++---------------- .../InputList/InputList.module.css | 25 -- .../form-components/InputList/InputList.tsx | 22 +- .../form-components/Slider/Slider.tsx | 30 ++- .../Slider/SliderWithInput.tsx | 39 +++ app/react/docker/containers/CreateView/.keep | 0 .../CreateView/ResourcesTab/DevicesField.tsx | 86 +++++++ .../ResourcesTab/EditResourceForm.tsx | 123 +++++++++ .../GpuFieldset/GpuFieldset.tsx} | 139 +++++----- .../ResourcesTab/GpuFieldset/index.ts | 14 + .../ResourcesTab/GpuFieldset/toRequest.ts | 33 +++ .../ResourcesTab/GpuFieldset/toViewModel.ts | 30 +++ .../ResourcesTab/GpuFieldset/types.ts | 6 + .../ResourcesTab/GpuFieldset/validation.ts | 14 + .../ResourcesTab/ResourcesFieldset.tsx | 103 ++++++++ .../CreateView/ResourcesTab/ResourcesTab.tsx | 148 +++++++++++ .../ResourcesTab/RuntimeSection.tsx | 74 ++++++ .../ResourcesTab/RuntimeSelector.tsx | 31 +++ .../CreateView/ResourcesTab/SysctlsField.tsx | 70 +++++ .../CreateView/ResourcesTab/index.ts | 15 ++ .../CreateView/ResourcesTab/memory-utils.ts | 36 +++ .../CreateView/ResourcesTab/toRequest.ts | 35 +++ .../CreateView/ResourcesTab/toViewModel.ts | 49 ++++ .../CreateView/ResourcesTab/validation.ts | 25 ++ .../containers/queries/useUpdateContainer.ts | 40 +++ 28 files changed, 1114 insertions(+), 537 deletions(-) create mode 100644 app/react/components/form-components/Slider/SliderWithInput.tsx delete mode 100644 app/react/docker/containers/CreateView/.keep create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/DevicesField.tsx create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/EditResourceForm.tsx rename app/react/docker/containers/CreateView/{Gpu.tsx => ResourcesTab/GpuFieldset/GpuFieldset.tsx} (97%) create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/index.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/toRequest.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/toViewModel.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/types.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/validation.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/ResourcesFieldset.tsx create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/ResourcesTab.tsx create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/RuntimeSection.tsx create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/RuntimeSelector.tsx create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/SysctlsField.tsx create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/index.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/memory-utils.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/toRequest.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/toViewModel.ts create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/validation.ts create mode 100644 app/react/docker/containers/queries/useUpdateContainer.ts diff --git a/app/docker/react/components/containers.ts b/app/docker/react/components/containers.ts index 2ce3ea260..1f573b4eb 100644 --- a/app/docker/react/components/containers.ts +++ b/app/docker/react/components/containers.ts @@ -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, NetworkTabValues>( [], networkTabUtils.validation ); + +withFormValidation, ResourcesTabValues>( + ngModule, + withUIRouter(withReactQuery(ResourcesTab)), + 'dockerCreateContainerResourcesTab', + [ + 'allowPrivilegedMode', + 'isDevicesFieldVisible', + 'isInitFieldVisible', + 'isSysctlFieldVisible', + 'isDuplicate', + 'isImageInvalid', + 'redeploy', + ], + resourcesTabUtils.validation +); diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index bbf37a899..583955818 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -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']) diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 25ea99a1e..3bbfca18c 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -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; diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index bf75ebd6a..a5acb7786 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -283,233 +283,21 @@
-
-
Runtime
- -
-
- -
-
- - -
-
- -
-
- - -
- -
- -
-
- -
-
- -
-
- - - add device - -
- -
-
-
- host - -
-
- container - -
- -
-
- -
- - -
-
- - add sysctl -
- -
-
-
- name - -
-
- value - -
- -
-
- -
- - -
- -
- -
-
-

Size of /dev/shm (MB)

-
-
- - -
-
GPU
- - -
- -
-
Resources
- -
- -
- -
-
- -
-
-
-
-
-
-

- Value must be between 0 and {{ state.sliderMaxMemory }}. -

-
-
-
- - -
- -
- -
-
- -
-
-
-
-
-
-

- Value must be between 0 and {{ state.sliderMaxMemory }}. -

-
-
-
- - -
- -
- -
-
- - -
-
- -
-
-

- - Updating any resource value to ‘unlimited' will redeploy this container. -

-
-
- -
-
+
diff --git a/app/react/components/form-components/InputList/InputList.module.css b/app/react/components/form-components/InputList/InputList.module.css index fe03fb917..e69de29bb 100644 --- a/app/react/components/form-components/InputList/InputList.module.css +++ b/app/react/components/form-components/InputList/InputList.module.css @@ -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; -} diff --git a/app/react/components/form-components/InputList/InputList.tsx b/app/react/components/form-components/InputList/InputList.tsx index 8ab9e6059..79ca4d082 100644 --- a/app/react/components/form-components/InputList/InputList.tsx +++ b/app/react/components/form-components/InputList/InputList.tsx @@ -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 extends readonly (infer ElementType)[] @@ -94,12 +92,9 @@ export function InputList({ }: Props) { const isAddButtonVisible = !(isAddButtonHidden || readOnly); return ( -
+
{label && ( -
+
{label} {tooltip && } @@ -121,14 +116,7 @@ export function InputList({ typeof errors === 'object' ? errors[index] : undefined; return ( -
+
{Item ? ( ({ )} {isAddButtonVisible && ( -
+