From fd916bc8a22174c4f5471ffe5cc8b7fcbdebad21 Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Fri, 3 Mar 2023 14:47:10 +1300 Subject: [PATCH] feat(gpu): rework docker GPU for UI performance [EE-4918] (#8518) --- api/cmd/portainer/main.go | 24 ++- api/datastore/migrate_post_init.go | 116 ++++++++++++++ api/datastore/migrator/migrate_dbversion90.go | 34 +++- .../test_data/output_24_to_latest.json | 2 + .../endpoints/endpoint_settings_update.go | 12 ++ api/kubernetes/cli/client.go | 23 +-- api/portainer.go | 3 + app/assets/css/vendor-override.css | 5 - .../imageRegistry/por-image-registry.html | 2 +- app/docker/react/components/index.ts | 9 +- .../create/createContainerController.js | 19 ++- .../containers/create/createcontainer.html | 31 ++-- app/docker/views/dashboard/dashboard.html | 7 +- ...ocker-features-configuration.controller.js | 71 +++++++-- .../docker-features-configuration.html | 43 ++++- app/portainer/react/components/envronments.ts | 2 +- app/portainer/react/components/index.ts | 7 + app/portainer/services/authentication.js | 1 - .../views/endpoints/edit/endpoint.html | 22 ++- .../endpoints/edit/endpointController.js | 25 +-- app/react-tools/test-mocks.ts | 1 + app/react/components/.keep | 0 .../InsightsBox/InsightsBox.stories.tsx | 18 +++ .../InsightsBox/InsightsBox.test.tsx | 16 ++ .../components/InsightsBox/InsightsBox.tsx | 63 ++++++++ app/react/components/InsightsBox/index.ts | 1 + .../components/InsightsBox/insights-store.ts | 35 +++++ .../InputList/InputList.module.css | 9 -- .../form-components/InputList/InputList.tsx | 148 ++++++++++-------- .../docker/containers/CreateView/Gpu.tsx | 39 +++-- .../ContainersDatatable/columns/index.tsx | 2 +- app/react/docker/containers/utils.ts | 16 +- .../host/SetupView}/GpusList.tsx | 5 +- .../environment.service/create.ts | 10 -- .../environments/queries/useEnvironment.ts | 11 +- app/react/portainer/environments/types.ts | 1 + .../WizardDocker/APITab/APIForm.tsx | 35 ++++- .../APITab/APIForm.validation.tsx | 3 - .../WizardDocker/APITab/APITab.tsx | 5 +- .../WizardDocker/APITab/types.ts | 2 - .../WizardDocker/SocketTab/SocketForm.tsx | 35 ++++- .../SocketTab/SocketForm.validation.tsx | 3 - .../WizardDocker/SocketTab/SocketTab.tsx | 8 +- .../WizardDocker/SocketTab/types.ts | 2 - .../WizardDocker/WizardDocker.tsx | 2 + .../shared/AgentForm/AgentForm.tsx | 7 +- .../shared/AgentForm/AgentForm.validation.tsx | 2 - .../EdgeAgentForm/EdgeAgentForm.tsx | 11 +- .../EdgeAgentForm/EdgeAgentForm.validation.ts | 2 - .../EdgeAgentTab/EdgeAgentForm/types.ts | 2 - .../shared/EdgeAgentTab/EdgeAgentTab.tsx | 3 - .../shared/Hardware/Hardware.tsx | 22 --- 52 files changed, 692 insertions(+), 285 deletions(-) create mode 100644 api/datastore/migrate_post_init.go delete mode 100644 app/react/components/.keep create mode 100644 app/react/components/InsightsBox/InsightsBox.stories.tsx create mode 100644 app/react/components/InsightsBox/InsightsBox.test.tsx create mode 100644 app/react/components/InsightsBox/InsightsBox.tsx create mode 100644 app/react/components/InsightsBox/index.ts create mode 100644 app/react/components/InsightsBox/insights-store.ts rename app/react/{portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware => docker/host/SetupView}/GpusList.tsx (90%) delete mode 100644 app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware.tsx diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 9a0285f0d..a2a0d545b 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -689,20 +689,16 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { log.Fatal().Err(err).Msg("failed initializing upgrade service") } - // FIXME: In 2.16 we changed the way ingress controller permissions are - // stored. Instead of being stored as annotation on an ingress rule, we keep - // them in our database. However, in order to run the migration we need an - // admin kube client to run lookup the old ingress rules and compare them - // with the current existing ingress classes. - // - // Unfortunately, our migrations run as part of the database initialization - // and our kubeclients require an initialized database. So it is not - // possible to do this migration as part of our normal flow. We DO have a - // migration which toggles a boolean in kubernetes configuration that - // indicated that this "post init" migration should be run. If/when this is - // resolved we can remove this function. - err = kubernetesClientFactory.PostInitMigrateIngresses() - if err != nil { + // Our normal migrations run as part of the database initialization + // but some more complex migrations require access to a kubernetes or docker + // client. Therefore we run a separate migration process just before + // starting the server. + postInitMigrator := datastore.NewPostInitMigrator( + kubernetesClientFactory, + dockerClientFactory, + dataStore, + ) + if err := postInitMigrator.PostInitMigrate(); err != nil { log.Fatal().Err(err).Msg("failure during post init migrations") } diff --git a/api/datastore/migrate_post_init.go b/api/datastore/migrate_post_init.go new file mode 100644 index 000000000..dab0139b6 --- /dev/null +++ b/api/datastore/migrate_post_init.go @@ -0,0 +1,116 @@ +package datastore + +import ( + "context" + + "github.com/docker/docker/api/types" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/docker" + "github.com/portainer/portainer/api/kubernetes/cli" + "github.com/rs/zerolog/log" +) + +type PostInitMigrator struct { + kubeFactory *cli.ClientFactory + dockerFactory *docker.ClientFactory + dataStore dataservices.DataStore +} + +func NewPostInitMigrator( + kubeFactory *cli.ClientFactory, + dockerFactory *docker.ClientFactory, + dataStore dataservices.DataStore, +) *PostInitMigrator { + return &PostInitMigrator{ + kubeFactory: kubeFactory, + dockerFactory: dockerFactory, + dataStore: dataStore, + } +} + +func (migrator *PostInitMigrator) PostInitMigrate() error { + if err := migrator.PostInitMigrateIngresses(); err != nil { + return err + } + + migrator.PostInitMigrateGPUs() + + return nil +} + +func (migrator *PostInitMigrator) PostInitMigrateIngresses() error { + endpoints, err := migrator.dataStore.Endpoint().Endpoints() + if err != nil { + return err + } + for i := range endpoints { + // Early exit if we do not need to migrate! + if endpoints[i].PostInitMigrations.MigrateIngresses == false { + return nil + } + + err := migrator.kubeFactory.MigrateEndpointIngresses(&endpoints[i]) + if err != nil { + log.Debug().Err(err).Msg("failure migrating endpoint ingresses") + } + } + + return nil +} + +// PostInitMigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found +// If there's an error getting the containers, we'll log it and move on +func (migrator *PostInitMigrator) PostInitMigrateGPUs() { + environments, err := migrator.dataStore.Endpoint().Endpoints() + if err != nil { + log.Err(err).Msg("failure getting endpoints") + return + } + for i := range environments { + if environments[i].Type == portainer.DockerEnvironment { + // // Early exit if we do not need to migrate! + if environments[i].PostInitMigrations.MigrateGPUs == false { + return + } + + // set the MigrateGPUs flag to false so we don't run this again + environments[i].PostInitMigrations.MigrateGPUs = false + migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i]) + + // create a docker client + dockerClient, err := migrator.dockerFactory.CreateClient(&environments[i], "", nil) + if err != nil { + log.Err(err).Msg("failure creating docker client for environment: " + environments[i].Name) + return + } + defer dockerClient.Close() + + // get all containers + containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true}) + if err != nil { + log.Err(err).Msg("failed to list containers") + return + } + + // check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole endpoint + containersLoop: + for _, container := range containers { + // https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs + containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID) + if err != nil { + log.Err(err).Msg("failed to inspect container") + return + } + deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests + for _, deviceRequest := range deviceRequests { + if deviceRequest.Driver == "nvidia" { + environments[i].EnableGPUManagement = true + migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i]) + break containersLoop + } + } + } + } + } +} diff --git a/api/datastore/migrator/migrate_dbversion90.go b/api/datastore/migrator/migrate_dbversion90.go index 4d890a40a..fe107188d 100644 --- a/api/datastore/migrator/migrate_dbversion90.go +++ b/api/datastore/migrator/migrate_dbversion90.go @@ -3,11 +3,16 @@ package migrator import ( "github.com/rs/zerolog/log" + portainer "github.com/portainer/portainer/api" portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors" ) func (m *Migrator) migrateDBVersionToDB90() error { - if err := m.updateUserThemForDB90(); err != nil { + if err := m.updateUserThemeForDB90(); err != nil { + return err + } + + if err := m.updateEnableGpuManagementFeatures(); err != nil { return err } @@ -39,7 +44,7 @@ func (m *Migrator) updateEdgeStackStatusForDB90() error { return nil } -func (m *Migrator) updateUserThemForDB90() error { +func (m *Migrator) updateUserThemeForDB90() error { log.Info().Msg("updating existing user theme settings") users, err := m.userService.Users() @@ -60,3 +65,28 @@ func (m *Migrator) updateUserThemForDB90() error { return nil } + +func (m *Migrator) updateEnableGpuManagementFeatures() error { + // get all environments + environments, err := m.endpointService.Endpoints() + if err != nil { + return err + } + + for _, environment := range environments { + if environment.Type == portainer.DockerEnvironment { + // set the PostInitMigrations.MigrateGPUs to true on this environment to run the migration only on the 2.18 upgrade + environment.PostInitMigrations.MigrateGPUs = true + // if there's one or more gpu, set the EnableGpuManagement setting to true + gpuList := environment.Gpus + if len(gpuList) > 0 { + environment.EnableGPUManagement = true + } + // update the environment + if err := m.endpointService.UpdateEndpoint(environment.ID, &environment); err != nil { + return err + } + } + } + return nil +} diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index a4d4b2ac8..d5c2e3be1 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -46,6 +46,7 @@ }, "EdgeCheckinInterval": 0, "EdgeKey": "", + "EnableGPUManagement": false, "Gpus": [], "GroupId": 1, "Id": 1, @@ -71,6 +72,7 @@ "LastCheckInDate": 0, "Name": "local", "PostInitMigrations": { + "MigrateGPUs": true, "MigrateIngresses": true }, "PublicURL": "", diff --git a/api/http/handler/endpoints/endpoint_settings_update.go b/api/http/handler/endpoints/endpoint_settings_update.go index 68713f385..34a9abf6a 100644 --- a/api/http/handler/endpoints/endpoint_settings_update.go +++ b/api/http/handler/endpoints/endpoint_settings_update.go @@ -28,6 +28,10 @@ type endpointSettingsUpdatePayload struct { AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"` // Whether host management features are enabled EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"` + + EnableGPUManagement *bool `json:"enableGPUManagement" example:"false"` + + Gpus []portainer.Pair `json:"gpus"` } func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error { @@ -107,6 +111,14 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures } + if payload.EnableGPUManagement != nil { + endpoint.EnableGPUManagement = *payload.EnableGPUManagement + } + + if payload.Gpus != nil { + endpoint.Gpus = payload.Gpus + } + endpoint.SecuritySettings = securitySettings err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint) diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index d15d486bb..fc77a7786 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -12,7 +12,6 @@ import ( "github.com/pkg/errors" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" - "github.com/rs/zerolog/log" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -221,27 +220,7 @@ func buildLocalClient() (*kubernetes.Clientset, error) { return kubernetes.NewForConfig(config) } -func (factory *ClientFactory) PostInitMigrateIngresses() error { - endpoints, err := factory.dataStore.Endpoint().Endpoints() - if err != nil { - return err - } - for i := range endpoints { - // Early exit if we do not need to migrate! - if endpoints[i].PostInitMigrations.MigrateIngresses == false { - return nil - } - - err := factory.migrateEndpointIngresses(&endpoints[i]) - if err != nil { - log.Debug().Err(err).Msg("failure migrating endpoint ingresses") - } - } - - return nil -} - -func (factory *ClientFactory) migrateEndpointIngresses(e *portainer.Endpoint) error { +func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint) error { // classes is a list of controllers which have been manually added to the // cluster setup view. These need to all be allowed globally, but then // blocked in specific namespaces which they were not previously allowed in. diff --git a/api/portainer.go b/api/portainer.go index 918c32aaa..49b53f97d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -402,6 +402,8 @@ type ( Version string `example:"1.0.0"` } + EnableGPUManagement bool `json:"EnableGPUManagement"` + // Deprecated fields // Deprecated in DBVersion == 4 TLS bool `json:"TLS,omitempty"` @@ -502,6 +504,7 @@ type ( // EndpointPostInitMigrations EndpointPostInitMigrations struct { MigrateIngresses bool `json:"MigrateIngresses"` + MigrateGPUs bool `json:"MigrateGPUs"` } // Extension represents a deprecated Portainer extension diff --git a/app/assets/css/vendor-override.css b/app/assets/css/vendor-override.css index 48bbfffc6..6135d55d9 100644 --- a/app/assets/css/vendor-override.css +++ b/app/assets/css/vendor-override.css @@ -358,11 +358,6 @@ input:-webkit-autofill { } /* Overide Vendor CSS */ - -.btn-link:hover { - color: var(--text-link-hover-color) !important; -} - .multiSelect.inlineBlock button { margin: 0; } diff --git a/app/docker/components/imageRegistry/por-image-registry.html b/app/docker/components/imageRegistry/por-image-registry.html index 142b3139a..22eab8cd5 100644 --- a/app/docker/components/imageRegistry/por-image-registry.html +++ b/app/docker/components/imageRegistry/por-image-registry.html @@ -36,7 +36,7 @@ title="Search image on Docker Hub" target="_blank" > - Search + Search diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index 9a5b71bdf..ee48460bc 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -37,5 +37,12 @@ export const componentsModule = angular ) .component( 'gpu', - r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus']) + r2a(Gpu, [ + 'values', + 'onChange', + 'gpus', + 'usedGpus', + 'usedAllGpus', + 'enableGpuManagement', + ]) ).name; diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 26cc19632..625bbf0ca 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -21,6 +21,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ '$timeout', '$transition$', '$filter', + '$analytics', 'Container', 'ContainerHelper', 'Image', @@ -35,6 +36,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ 'FormValidator', 'RegistryService', 'SystemService', + 'SettingsService', 'PluginService', 'HttpRequestHelper', 'endpoint', @@ -46,6 +48,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $timeout, $transition$, $filter, + $analytics, Container, ContainerHelper, Image, @@ -60,6 +63,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ FormValidator, RegistryService, SystemService, + SettingsService, PluginService, HttpRequestHelper, endpoint @@ -1042,6 +1046,18 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); } + async function sendAnalytics() { + const publicSettings = await SettingsService.publicSettings(); + const analyticsAllowed = publicSettings.EnableTelemetry; + const image = `${$scope.formValues.RegistryModel.Registry.URL}/${$scope.formValues.RegistryModel.Image}`; + if (analyticsAllowed && $scope.formValues.GPU.enabled) { + $analytics.eventTrack('gpuContainerCreated', { + category: 'docker', + metadata: { gpu: $scope.formValues.GPU, containerImage: image }, + }); + } + } + function applyResourceControl(newContainer) { const userId = Authentication.getUserDetails().ID; const resourceControl = newContainer.Portainer.ResourceControl; @@ -1101,7 +1117,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [ return validateForm(accessControlData, $scope.isAdmin); } - function onSuccess() { + async function onSuccess() { + await sendAnalytics(); Notifications.success('Success', 'Container successfully created'); $state.go('docker.containers', {}, { reload: true }); } diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index e46ee11e6..63abdb1da 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -17,8 +17,8 @@
- -
+ +
@@ -37,8 +37,6 @@ model="formValues.RegistryModel" ng-if="formValues.RegistryModel.Registry" auto-complete="true" - label-class="col-sm-1" - input-class="col-sm-11" endpoint="endpoint" is-admin="isAdmin" check-rate-limits="formValues.alwaysPull" @@ -169,7 +167,7 @@
diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js index 3fcc4cbb1..a6251013a 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js @@ -2,10 +2,13 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums'; export default class DockerFeaturesConfigurationController { /* @ngInject */ - constructor($async, $scope, EndpointService, Notifications, StateManager) { + constructor($async, $scope, $state, $analytics, EndpointService, SettingsService, Notifications, StateManager) { this.$async = $async; this.$scope = $scope; + this.$state = $state; + this.$analytics = $analytics; this.EndpointService = EndpointService; + this.SettingsService = SettingsService; this.Notifications = Notifications; this.StateManager = StateManager; @@ -35,6 +38,8 @@ export default class DockerFeaturesConfigurationController { this.save = this.save.bind(this); this.onChangeField = this.onChangeField.bind(this); this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this); + this.onToggleGPUManagement = this.onToggleGPUManagement.bind(this); + this.onGpusChange = this.onGpusChange.bind(this); this.onChangeEnableHostManagementFeatures = this.onChangeField('enableHostManagementFeatures'); this.onChangeAllowVolumeBrowserForRegularUsers = this.onChangeField('allowVolumeBrowserForRegularUsers'); this.onChangeDisableBindMountsForRegularUsers = this.onChangeField('disableBindMountsForRegularUsers'); @@ -52,6 +57,12 @@ export default class DockerFeaturesConfigurationController { }); } + onToggleGPUManagement(checked) { + this.$scope.$evalAsync(() => { + this.state.enableGPUManagement = checked; + }); + } + onChange(values) { return this.$scope.$evalAsync(() => { this.formValues = { @@ -69,6 +80,12 @@ export default class DockerFeaturesConfigurationController { }; } + onGpusChange(value) { + return this.$async(async () => { + this.endpoint.Gpus = value; + }); + } + isContainerEditDisabled() { const { disableBindMountsForRegularUsers, @@ -92,7 +109,11 @@ export default class DockerFeaturesConfigurationController { return this.$async(async () => { try { this.state.actionInProgress = true; - const securitySettings = { + + const validGpus = this.endpoint.Gpus.filter((gpu) => gpu.name && gpu.value); + const gpus = this.state.enableGPUManagement ? validGpus : []; + + const settings = { enableHostManagementFeatures: this.formValues.enableHostManagementFeatures, allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers, allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers, @@ -102,33 +123,53 @@ export default class DockerFeaturesConfigurationController { allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers, allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers, allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers, + enableGPUManagement: this.state.enableGPUManagement, + gpus, }; - await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings); + const publicSettings = await this.SettingsService.publicSettings(); + const analyticsAllowed = publicSettings.EnableTelemetry; + if (analyticsAllowed) { + // send analytics if GPU management is changed (with the new state) + if (this.initialEnableGPUManagement !== this.state.enableGPUManagement) { + this.$analytics.eventTrack('enable-gpu-management-updated', { category: 'portainer', metadata: { enableGPUManagementState: this.state.enableGPUManagement } }); + } + // send analytics if the number of GPUs is changed (with a list of the names) + if (gpus.length > this.initialGPUs.length) { + const numberOfGPUSAdded = this.endpoint.Gpus.length - this.initialGPUs.length; + this.$analytics.eventTrack('gpus-added', { category: 'portainer', metadata: { gpus: gpus.map((gpu) => gpu.name), numberOfGPUSAdded } }); + } + if (gpus.length < this.initialGPUs.length) { + const numberOfGPUSRemoved = this.initialGPUs.length - this.endpoint.Gpus.length; + this.$analytics.eventTrack('gpus-removed', { category: 'portainer', metadata: { gpus: gpus.map((gpu) => gpu.name), numberOfGPUSRemoved } }); + } + this.initialGPUs = gpus; + this.initialEnableGPUManagement = this.state.enableGPUManagement; + } - this.endpoint.SecuritySettings = securitySettings; + await this.EndpointService.updateSecuritySettings(this.endpoint.Id, settings); + + this.endpoint.SecuritySettings = settings; this.Notifications.success('Success', 'Saved settings successfully'); } catch (e) { this.Notifications.error('Failure', e, 'Failed saving settings'); } this.state.actionInProgress = false; + this.$state.reload(); }); } - checkAgent() { - const applicationState = this.StateManager.getState(); - return applicationState.endpoint.mode.agentProxy; - } - $onInit() { const securitySettings = this.endpoint.SecuritySettings; - const isAgent = this.checkAgent(); - this.isAgent = isAgent; + const applicationState = this.StateManager.getState(); + this.isAgent = applicationState.endpoint.mode.agentProxy; + + this.isDockerStandaloneEnv = applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'; this.formValues = { - enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures, - allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers, + enableHostManagementFeatures: this.isAgent && securitySettings.enableHostManagementFeatures, + allowVolumeBrowserForRegularUsers: this.isAgent && securitySettings.allowVolumeBrowserForRegularUsers, disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers, disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers, disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers, @@ -137,5 +178,9 @@ export default class DockerFeaturesConfigurationController { disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers, disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers, }; + + this.state.enableGPUManagement = this.isDockerStandaloneEnv && (this.endpoint.EnableGPUManagement || this.endpoint.Gpus.length > 0); + this.initialGPUs = this.endpoint.Gpus; + this.initialEnableGPUManagement = this.endpoint.EnableGPUManagement; } } diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.html b/app/docker/views/docker-features-configuration/docker-features-configuration.html index 3e93ac720..08ddd4f65 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.html +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.html @@ -150,9 +150,42 @@
Other
+
+ +
+
+
+
+ +
+
+
+ Actions
- diff --git a/app/portainer/react/components/envronments.ts b/app/portainer/react/components/envronments.ts index 4e7f9ae68..73fa82ef6 100644 --- a/app/portainer/react/components/envronments.ts +++ b/app/portainer/react/components/envronments.ts @@ -4,7 +4,7 @@ import { r2a } from '@/react-tools/react2angular'; import { withControlledInput } from '@/react-tools/withControlledInput'; import { EdgeKeyDisplay } from '@/react/portainer/environments/ItemView/EdgeKeyDisplay'; import { KVMControl } from '@/react/portainer/environments/KvmView/KVMControl'; -import { GpusList } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; +import { GpusList } from '@/react/docker/host/SetupView/GpusList'; export const environmentsModule = angular .module('portainer.app.react.components.environments', []) diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts index 2bab4d0c8..e949a5013 100644 --- a/app/portainer/react/components/index.ts +++ b/app/portainer/react/components/index.ts @@ -25,6 +25,7 @@ import { Slider } from '@@/form-components/Slider'; import { TagButton } from '@@/TagButton'; import { BETeaserButton } from '@@/BETeaserButton'; import { CodeEditor } from '@@/CodeEditor'; +import { InsightsBox } from '@@/InsightsBox'; import { fileUploadField } from './file-upload-field'; import { switchField } from './switch-field'; @@ -32,6 +33,7 @@ import { customTemplatesModule } from './custom-templates'; import { gitFormModule } from './git-form'; import { settingsModule } from './settings'; import { accessControlModule } from './access-control'; +import { environmentsModule } from './envronments'; import { envListModule } from './enviroments-list-view-components'; export const componentsModule = angular @@ -40,6 +42,7 @@ export const componentsModule = angular gitFormModule, settingsModule, accessControlModule, + environmentsModule, envListModule, ]) .component( @@ -74,6 +77,10 @@ export const componentsModule = angular .component('badge', r2a(Badge, ['type', 'className'])) .component('fileUploadField', fileUploadField) .component('porSwitchField', switchField) + .component( + 'insightsBox', + r2a(InsightsBox, ['header', 'content', 'setHtmlContent', 'insightCloseId']) + ) .component( 'passwordCheckHint', r2a(withReactQuery(PasswordCheckHint), [ diff --git a/app/portainer/services/authentication.js b/app/portainer/services/authentication.js index 1ffc9b1a1..02f85654d 100644 --- a/app/portainer/services/authentication.js +++ b/app/portainer/services/authentication.js @@ -36,7 +36,6 @@ angular.module('portainer.app').factory('Authentication', [ await setUser(jwt); return true; } catch (error) { - console.log('Unable to initialize authentication service', error); return tryAutoLoginExtension(); } } diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 7d57c1406..e418c4238 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -210,14 +210,30 @@
-
Hardware acceleration
- +
+ +
+ ); +} diff --git a/app/react/components/InsightsBox/index.ts b/app/react/components/InsightsBox/index.ts new file mode 100644 index 000000000..c02dcafa9 --- /dev/null +++ b/app/react/components/InsightsBox/index.ts @@ -0,0 +1 @@ +export { InsightsBox } from './InsightsBox'; diff --git a/app/react/components/InsightsBox/insights-store.ts b/app/react/components/InsightsBox/insights-store.ts new file mode 100644 index 000000000..a35881158 --- /dev/null +++ b/app/react/components/InsightsBox/insights-store.ts @@ -0,0 +1,35 @@ +import { createStore } from 'zustand'; +import { persist } from 'zustand/middleware'; + +import { keyBuilder } from '@/react/hooks/useLocalStorage'; + +interface InsightsStore { + insightIDsClosed: string[]; + addInsightIDClosed: (insightIDClosed: string) => void; + isClosed: (insightID?: string) => boolean; +} + +export const insightStore = createStore()( + persist( + (set, get) => ({ + insightIDsClosed: [], + addInsightIDClosed: (insightIDClosed: string) => { + set((state) => { + const currentIDsClosed = state.insightIDsClosed || []; + return { insightIDsClosed: [...currentIDsClosed, insightIDClosed] }; + }); + }, + isClosed: (insightID?: string) => { + if (!insightID) { + return false; + } + const currentIDsClosed = get().insightIDsClosed || []; + return currentIDsClosed.includes(insightID); + }, + }), + { + name: keyBuilder('insightIDsClosed'), + getStorage: () => localStorage, + } + ) +); diff --git a/app/react/components/form-components/InputList/InputList.module.css b/app/react/components/form-components/InputList/InputList.module.css index b1c67ea6c..fe03fb917 100644 --- a/app/react/components/form-components/InputList/InputList.module.css +++ b/app/react/components/form-components/InputList/InputList.module.css @@ -1,7 +1,3 @@ -.items { - margin-top: 10px; -} - .items > * + * { margin-top: 2px; } @@ -24,11 +20,6 @@ margin-bottom: 20px; } -.item-actions { - display: flex; - margin-left: 2px; -} - .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 a2a5f9c04..89540d5d5 100644 --- a/app/react/components/form-components/InputList/InputList.tsx +++ b/app/react/components/form-components/InputList/InputList.tsx @@ -1,9 +1,9 @@ import { ComponentType } from 'react'; import clsx from 'clsx'; import { FormikErrors } from 'formik'; -import { ArrowDown, ArrowUp, Trash2 } from 'lucide-react'; +import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react'; -import { AddButton, Button } from '@@/buttons'; +import { Button } from '@@/buttons'; import { Tooltip } from '@@/Tip/Tooltip'; import { TextTip } from '@@/Tip/TextTip'; @@ -79,18 +79,10 @@ export function InputList({ return (
-
+ {label} {tooltip && } -
- {!(isAddButtonHidden || readOnly) && ( - - )} +
{textTip && ( @@ -99,68 +91,86 @@ export function InputList({
)} -
- {value.map((item, index) => { - const key = itemKeyGetter(item, index); - const error = typeof errors === 'object' ? errors[index] : undefined; + {value.length > 0 && ( +
+ {value.map((item, index) => { + const key = itemKeyGetter(item, index); + const error = + typeof errors === 'object' ? errors[index] : undefined; - return ( -
- {Item ? ( - handleChangeItem(key, value)} - error={error} - disabled={disabled} - readOnly={readOnly} - /> - ) : ( - renderItem( - item, - (value: T) => handleChangeItem(key, value), - error - ) - )} -
- {!readOnly && movable && ( - <> -
-
- ); - })} + ); + })} +
+ )} +
+ {!(isAddButtonHidden || readOnly) && ( + + )}
); diff --git a/app/react/docker/containers/CreateView/Gpu.tsx b/app/react/docker/containers/CreateView/Gpu.tsx index e26114097..fc72f5582 100644 --- a/app/react/docker/containers/CreateView/Gpu.tsx +++ b/app/react/docker/containers/CreateView/Gpu.tsx @@ -10,6 +10,7 @@ import { OptionProps } from 'react-select/dist/declarations/src/components/Optio import { Select } from '@@/form-components/ReactSelect'; import { Switch } from '@@/form-components/SwitchField/Switch'; import { Tooltip } from '@@/Tip/Tooltip'; +import { TextTip } from '@@/Tip/TextTip'; interface Values { enabled: boolean; @@ -35,6 +36,7 @@ export interface Props { gpus: GPU[]; usedGpus: string[]; usedAllGpus: boolean; + enableGpuManagement?: boolean; } const NvidiaCapabilitiesOptions = [ @@ -103,6 +105,7 @@ export function Gpu({ gpus = [], usedGpus = [], usedAllGpus, + enableGpuManagement, }: Props) { const options = useMemo(() => { const options = (gpus || []).map((gpu) => ({ @@ -181,30 +184,38 @@ export function Gpu({ return (
+ {!enableGpuManagement && ( + + GPU in the UI is not currently enabled for this environment. + + )}
Enable GPU
-
- - isMulti - closeMenuOnSelect - value={gpuValue} - isClearable={false} - backspaceRemovesValue={false} - isDisabled={!values.enabled} - onChange={onChangeSelectedGpus} - options={options} - components={{ MultiValueRemove }} - /> -
+ {enableGpuManagement && values.enabled && ( +
+ + isMulti + closeMenuOnSelect + value={gpuValue} + isClearable={false} + backspaceRemovesValue={false} + isDisabled={!values.enabled} + onChange={onChangeSelectedGpus} + options={options} + components={{ MultiValueRemove }} + /> +
+ )}
{values.enabled && ( diff --git a/app/react/docker/containers/ListView/ContainersDatatable/columns/index.tsx b/app/react/docker/containers/ListView/ContainersDatatable/columns/index.tsx index a645e6216..d1a0ba854 100644 --- a/app/react/docker/containers/ListView/ContainersDatatable/columns/index.tsx +++ b/app/react/docker/containers/ListView/ContainersDatatable/columns/index.tsx @@ -15,7 +15,7 @@ import { gpus } from './gpus'; export function useColumns( isHostColumnVisible: boolean, - isGPUsColumnVisible: boolean + isGPUsColumnVisible?: boolean ) { return useMemo( () => diff --git a/app/react/docker/containers/utils.ts b/app/react/docker/containers/utils.ts index 54c785bb2..6bb0ee242 100644 --- a/app/react/docker/containers/utils.ts +++ b/app/react/docker/containers/utils.ts @@ -1,8 +1,9 @@ import _ from 'lodash'; -import { useInfo } from '@/docker/services/system.service'; -import { EnvironmentId } from '@/react/portainer/environments/types'; import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; +import { EnvironmentId } from '@/react/portainer/environments/types'; +import { useInfo } from '@/docker/services/system.service'; +import { useEnvironment } from '@/react/portainer/environments/queries'; import { DockerContainer, ContainerStatus } from './types'; import { DockerContainerResponse } from './types/response'; @@ -95,10 +96,13 @@ function createStatus(statusText = ''): ContainerStatus { } export function useShowGPUsColumn(environmentID: EnvironmentId) { - const envInfoQuery = useInfo( + const isDockerStandaloneQuery = useInfo( environmentID, - (info) => !!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable + (info) => !(!!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable) // is not a swarm environment, therefore docker standalone ); - - return envInfoQuery.data !== true && !envInfoQuery.isLoading; + const enableGPUManagementQuery = useEnvironment( + environmentID, + (env) => env?.EnableGPUManagement + ); + return isDockerStandaloneQuery.data && enableGPUManagementQuery.data; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList.tsx b/app/react/docker/host/SetupView/GpusList.tsx similarity index 90% rename from app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList.tsx rename to app/react/docker/host/SetupView/GpusList.tsx index 738d9e3ca..1f3397cb7 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList.tsx +++ b/app/react/docker/host/SetupView/GpusList.tsx @@ -48,11 +48,12 @@ function Item({ item, onChange }: ItemProps) { export function GpusList({ value, onChange }: Props) { return ( - label="GPU" + label="GPUs" + tooltip="You may optionally set up the GPUs that will be selectable against containers, although 'All GPUs' will always be available." value={value} onChange={onChange} itemBuilder={() => ({ value: '', name: '' })} - addLabel="add" + addLabel="Add GPU" item={Item} /> ); diff --git a/app/react/portainer/environments/environment.service/create.ts b/app/react/portainer/environments/environment.service/create.ts index 78347d0ef..ba3243e1a 100644 --- a/app/react/portainer/environments/environment.service/create.ts +++ b/app/react/portainer/environments/environment.service/create.ts @@ -1,4 +1,3 @@ -import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types'; import { type TagId } from '@/portainer/tags/types'; @@ -18,7 +17,6 @@ interface CreateLocalDockerEnvironment { socketPath?: string; publicUrl?: string; meta?: EnvironmentMetadata; - gpus?: Gpu[]; } export async function createLocalDockerEnvironment({ @@ -26,7 +24,6 @@ export async function createLocalDockerEnvironment({ socketPath = '', publicUrl = '', meta = { tagIds: [] }, - gpus = [], }: CreateLocalDockerEnvironment) { const url = prefixPath(socketPath); @@ -37,7 +34,6 @@ export async function createLocalDockerEnvironment({ url, publicUrl, meta, - gpus, } ); @@ -113,7 +109,6 @@ export interface EnvironmentOptions { azure?: AzureSettings; tls?: TLSSettings; isEdgeDevice?: boolean; - gpus?: Gpu[]; pollFrequency?: number; edge?: EdgeSettings; tunnelServerAddr?: string; @@ -145,7 +140,6 @@ export interface CreateAgentEnvironmentValues { name: string; environmentUrl: string; meta: EnvironmentMetadata; - gpus: Gpu[]; } export function createAgentEnvironment({ @@ -173,7 +167,6 @@ interface CreateEdgeAgentEnvironment { tunnelServerAddr?: string; meta?: EnvironmentMetadata; pollFrequency: number; - gpus?: Gpu[]; isEdgeDevice?: boolean; edge: EdgeSettings; } @@ -182,7 +175,6 @@ export function createEdgeAgentEnvironment({ name, portainerUrl, meta = { tagIds: [] }, - gpus = [], isEdgeDevice, pollFrequency, edge, @@ -196,7 +188,6 @@ export function createEdgeAgentEnvironment({ skipVerify: true, skipClientVerify: true, }, - gpus, isEdgeDevice, pollFrequency, edge, @@ -226,7 +217,6 @@ async function createEnvironment( TagIds: arrayToJson(tagIds), CheckinInterval: options.pollFrequency, IsEdgeDevice: options.isEdgeDevice, - Gpus: arrayToJson(options.gpus), }; const { tls, azure } = options; diff --git a/app/react/portainer/environments/queries/useEnvironment.ts b/app/react/portainer/environments/queries/useEnvironment.ts index 63f98e031..0ce1563e3 100644 --- a/app/react/portainer/environments/queries/useEnvironment.ts +++ b/app/react/portainer/environments/queries/useEnvironment.ts @@ -1,11 +1,18 @@ import { useQuery } from 'react-query'; import { getEndpoint } from '@/react/portainer/environments/environment.service'; -import { EnvironmentId } from '@/react/portainer/environments/types'; +import { + Environment, + EnvironmentId, +} from '@/react/portainer/environments/types'; import { withError } from '@/react-tools/react-query'; -export function useEnvironment(id?: EnvironmentId) { +export function useEnvironment( + id?: EnvironmentId, + select?: (environment: Environment | null) => T +) { return useQuery(['environments', id], () => (id ? getEndpoint(id) : null), { + select, ...withError('Failed loading environment'), staleTime: 50, enabled: !!id, diff --git a/app/react/portainer/environments/types.ts b/app/react/portainer/environments/types.ts index 418e338e0..639bad9f1 100644 --- a/app/react/portainer/environments/types.ts +++ b/app/react/portainer/environments/types.ts @@ -131,6 +131,7 @@ export type Environment = { TagIds: TagId[]; GroupId: EnvironmentGroupId; DeploymentOptions: DeploymentOptions | null; + EnableGPUManagement: boolean; EdgeID?: string; EdgeKey: string; EdgeCheckinInterval?: number; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx index c1c23ed3f..b2c95b0b1 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx @@ -3,7 +3,6 @@ import { useReducer } from 'react'; import { Plug2 } from 'lucide-react'; import { useCreateRemoteEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; -import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware'; import { notifySuccess } from '@/portainer/services/notifications'; import { Environment, @@ -13,6 +12,7 @@ import { import { LoadingButton } from '@@/buttons/LoadingButton'; import { FormControl } from '@@/form-components/FormControl'; import { Input } from '@@/form-components/Input'; +import { InsightsBox } from '@@/InsightsBox'; import { NameField } from '../../shared/NameField'; import { MoreSettingsSection } from '../../shared/MoreSettingsSection'; @@ -23,9 +23,10 @@ import { TLSFieldset } from './TLSFieldset'; interface Props { onCreate(environment: Environment): void; + isDockerStandalone?: boolean; } -export function APIForm({ onCreate }: Props) { +export function APIForm({ onCreate, isDockerStandalone }: Props) { const [formKey, clearForm] = useReducer((state) => state + 1, 0); const initialValues: FormValues = { url: '', @@ -35,7 +36,6 @@ export function APIForm({ onCreate }: Props) { groupId: 1, tagIds: [], }, - gpus: [], }; const mutation = useCreateRemoteEnvironmentMutation( @@ -73,7 +73,33 @@ export function APIForm({ onCreate }: Props) { - + {isDockerStandalone && ( + +

+ From 2.18 on, the set-up of available GPUs for a Docker + Standalone environment has been shifted from Add + environment and Environment details to Host -> Setup, + so as to align with other settings. +

+

+ A toggle has been introduced for enabling/disabling + management of GPU settings in the Portainer UI - to + alleviate the performance impact of showing those + settings. +

+

+ The UI has been updated to clarify that GPU settings + support is only for Docker Standalone (and not Docker + Swarm, which was never supported in the UI). +

+ + } + header="GPU settings update" + insightCloseId="gpu-settings-update-closed" + /> + )}
@@ -104,7 +130,6 @@ export function APIForm({ onCreate }: Props) { options: { tls, meta: values.meta, - gpus: values.gpus, }, }, { diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx index d34f49a60..89a935735 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx @@ -1,7 +1,5 @@ import { boolean, object, SchemaOf, string } from 'yup'; -import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; - import { metadataValidation } from '../../shared/MetadataFieldset/validation'; import { useNameValidation } from '../../shared/NameField'; @@ -16,6 +14,5 @@ export function useValidation(): SchemaOf { skipVerify: boolean(), meta: metadataValidation(), ...certsValidation(), - gpus: gpusListValidation(), }); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx index 3b39cd44d..21cd917fa 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx @@ -4,12 +4,13 @@ import { APIForm } from './APIForm'; interface Props { onCreate(environment: Environment): void; + isDockerStandalone?: boolean; } -export function APITab({ onCreate }: Props) { +export function APITab({ onCreate, isDockerStandalone }: Props) { return (
- +
); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts index 4112267d5..2d2143ccb 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts @@ -1,4 +1,3 @@ -import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export interface FormValues { @@ -10,5 +9,4 @@ export interface FormValues { certFile?: File; keyFile?: File; meta: EnvironmentMetadata; - gpus?: Gpu[]; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx index d45dea166..b6ed8c920 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx @@ -3,7 +3,6 @@ import { useReducer } from 'react'; import { Plug2 } from 'lucide-react'; import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; -import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware'; import { notifySuccess } from '@/portainer/services/notifications'; import { Environment } from '@/react/portainer/environments/types'; @@ -11,6 +10,7 @@ import { LoadingButton } from '@@/buttons/LoadingButton'; import { FormControl } from '@@/form-components/FormControl'; import { Input } from '@@/form-components/Input'; import { SwitchField } from '@@/form-components/SwitchField'; +import { InsightsBox } from '@@/InsightsBox'; import { NameField } from '../../shared/NameField'; import { MoreSettingsSection } from '../../shared/MoreSettingsSection'; @@ -20,16 +20,16 @@ import { FormValues } from './types'; interface Props { onCreate(environment: Environment): void; + isDockerStandalone?: boolean; } -export function SocketForm({ onCreate }: Props) { +export function SocketForm({ onCreate, isDockerStandalone }: Props) { const [formKey, clearForm] = useReducer((state) => state + 1, 0); const initialValues: FormValues = { name: '', socketPath: '', overridePath: false, meta: { groupId: 1, tagIds: [] }, - gpus: [], }; const mutation = useCreateLocalDockerEnvironmentMutation(); @@ -50,7 +50,33 @@ export function SocketForm({ onCreate }: Props) { - + {isDockerStandalone && ( + +

+ From 2.18 on, the set-up of available GPUs for a Docker + Standalone environment has been shifted from Add + environment and Environment details to Host -> Setup, + so as to align with other settings. +

+

+ A toggle has been introduced for enabling/disabling + management of GPU settings in the Portainer UI - to + alleviate the performance impact of showing those + settings. +

+

+ The UI has been updated to clarify that GPU settings + support is only for Docker Standalone (and not Docker + Swarm, which was never supported in the UI). +

+ + } + header="GPU settings update" + insightCloseId="gpu-settings-update-closed" + /> + )}
@@ -76,7 +102,6 @@ export function SocketForm({ onCreate }: Props) { { name: values.name, socketPath: values.overridePath ? values.socketPath : '', - gpus: values.gpus, meta: values.meta, }, { diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx index 6f336e820..f8b48cf0d 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx @@ -1,7 +1,5 @@ import { boolean, object, SchemaOf, string } from 'yup'; -import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; - import { metadataValidation } from '../../shared/MetadataFieldset/validation'; import { useNameValidation } from '../../shared/NameField'; @@ -21,6 +19,5 @@ export function useValidation(): SchemaOf { ) : schema ), - gpus: gpusListValidation(), }); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx index a93b0e253..3fbbf6281 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx @@ -6,15 +6,19 @@ import { SocketForm } from './SocketForm'; interface Props { onCreate(environment: Environment): void; + isDockerStandalone?: boolean; } -export function SocketTab({ onCreate }: Props) { +export function SocketTab({ onCreate, isDockerStandalone }: Props) { return ( <>
- +
); diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts index 84622bd39..9bc7fdad1 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts @@ -1,4 +1,3 @@ -import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export interface FormValues { @@ -6,5 +5,4 @@ export interface FormValues { socketPath: string; overridePath: boolean; meta: EnvironmentMetadata; - gpus: Gpu[]; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx index 4e8fb2746..181dc2885 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx @@ -109,12 +109,14 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) { return ( onCreate(environment, 'dockerApi')} + isDockerStandalone={isDockerStandalone} /> ); case 'socket': return ( onCreate(environment, 'localEndpoint')} + isDockerStandalone={isDockerStandalone} /> ); case 'edgeAgentStandard': diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx index dc0eeacde..dcedcc512 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx @@ -11,14 +11,12 @@ import { LoadingButton } from '@@/buttons/LoadingButton'; import { NameField } from '../NameField'; import { MoreSettingsSection } from '../MoreSettingsSection'; -import { Hardware } from '../Hardware/Hardware'; import { EnvironmentUrlField } from './EnvironmentUrlField'; import { useValidation } from './AgentForm.validation'; interface Props { onCreate(environment: Environment): void; - showGpus?: boolean; } const initialValues: CreateAgentEnvironmentValues = { @@ -28,10 +26,9 @@ const initialValues: CreateAgentEnvironmentValues = { groupId: 1, tagIds: [], }, - gpus: [], }; -export function AgentForm({ onCreate, showGpus = false }: Props) { +export function AgentForm({ onCreate }: Props) { const [formKey, clearForm] = useReducer((state) => state + 1, 0); const mutation = useCreateAgentEnvironmentMutation(); @@ -50,7 +47,7 @@ export function AgentForm({ onCreate, showGpus = false }: Props) { - {showGpus && } +
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx index 003c6b253..4829d9bbf 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx @@ -1,6 +1,5 @@ import { object, SchemaOf, string } from 'yup'; -import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create'; import { metadataValidation } from '../MetadataFieldset/validation'; @@ -11,7 +10,6 @@ export function useValidation(): SchemaOf { name: useNameValidation(), environmentUrl: environmentValidation(), meta: metadataValidation(), - gpus: gpusListValidation(), }); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx index 8d260520c..fe0d5e0fe 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx @@ -17,7 +17,6 @@ import { FormSection } from '@@/form-components/FormSection'; import { LoadingButton } from '@@/buttons/LoadingButton'; import { MoreSettingsSection } from '../../MoreSettingsSection'; -import { Hardware } from '../../Hardware/Hardware'; import { EdgeAgentFieldset } from './EdgeAgentFieldset'; import { useValidationSchema } from './EdgeAgentForm.validation'; @@ -26,16 +25,10 @@ import { FormValues } from './types'; interface Props { onCreate(environment: Environment): void; readonly: boolean; - showGpus?: boolean; asyncMode: boolean; } -export function EdgeAgentForm({ - onCreate, - readonly, - asyncMode, - showGpus = false, -}: Props) { +export function EdgeAgentForm({ onCreate, readonly, asyncMode }: Props) { const settingsQuery = useSettings(); const createMutation = useCreateEdgeAgentEnvironmentMutation(); @@ -76,7 +69,6 @@ export function EdgeAgentForm({ /> )} - {showGpus && } {!readonly && ( @@ -133,6 +125,5 @@ export function buildInitialValues(settings: Settings): FormValues { PingInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT, SnapshotInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT, }, - gpus: [], }; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts index 5a2677777..b34c3fe2f 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts @@ -4,7 +4,6 @@ import { edgeAsyncIntervalsValidation, EdgeAsyncIntervalsValues, } from '@/react/edge/components/EdgeAsyncIntervalsForm'; -import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { validation as urlValidation } from '@/react/portainer/common/PortainerTunnelAddrField'; import { validation as addressValidation } from '@/react/portainer/common/PortainerUrlField'; import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; @@ -23,7 +22,6 @@ export function useValidationSchema(asyncMode: boolean): SchemaOf { tunnelServerAddr: asyncMode ? string() : addressValidation(), pollFrequency: number().required(), meta: metadataValidation(), - gpus: gpusListValidation(), edge: isBE ? edgeAsyncIntervalsValidation() : (null as unknown as SchemaOf), diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts index 0ad061cf9..ddbdbb772 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts @@ -1,5 +1,4 @@ import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm'; -import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export interface FormValues { @@ -9,7 +8,6 @@ export interface FormValues { tunnelServerAddr?: string; pollFrequency: number; meta: EnvironmentMetadata; - gpus: Gpu[]; edge: EdgeAsyncIntervalsValues; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx index 6739c2f26..ca9d0774e 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx @@ -15,7 +15,6 @@ interface Props { onCreate: (environment: Environment) => void; commands: CommandTab[] | Partial>; isNomadTokenVisible?: boolean; - showGpus?: boolean; asyncMode?: boolean; } @@ -23,7 +22,6 @@ export function EdgeAgentTab({ onCreate, commands, isNomadTokenVisible, - showGpus = false, asyncMode = false, }: Props) { const [edgeInfo, setEdgeInfo] = useState(); @@ -35,7 +33,6 @@ export function EdgeAgentTab({ onCreate={handleCreate} readonly={!!edgeInfo} key={formKey} - showGpus={showGpus} asyncMode={asyncMode} /> diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware.tsx deleted file mode 100644 index 7e403113b..000000000 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useField } from 'formik'; - -import { - Gpu, - GpusList, -} from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; - -import { FormSection } from '@@/form-components/FormSection'; - -export function Hardware() { - const [field, , helpers] = useField('gpus'); - - function onChange(value: Gpu[]) { - helpers.setValue(value); - } - - return ( - - - - ); -}