mirror of https://github.com/portainer/portainer
feat(gpu): rework docker GPU for UI performance [EE-4918] (#8518)
parent
769c8372fb
commit
fd916bc8a2
|
@ -689,20 +689,16 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: In 2.16 we changed the way ingress controller permissions are
|
// Our normal migrations run as part of the database initialization
|
||||||
// stored. Instead of being stored as annotation on an ingress rule, we keep
|
// but some more complex migrations require access to a kubernetes or docker
|
||||||
// them in our database. However, in order to run the migration we need an
|
// client. Therefore we run a separate migration process just before
|
||||||
// admin kube client to run lookup the old ingress rules and compare them
|
// starting the server.
|
||||||
// with the current existing ingress classes.
|
postInitMigrator := datastore.NewPostInitMigrator(
|
||||||
//
|
kubernetesClientFactory,
|
||||||
// Unfortunately, our migrations run as part of the database initialization
|
dockerClientFactory,
|
||||||
// and our kubeclients require an initialized database. So it is not
|
dataStore,
|
||||||
// possible to do this migration as part of our normal flow. We DO have a
|
)
|
||||||
// migration which toggles a boolean in kubernetes configuration that
|
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||||
// 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 {
|
|
||||||
log.Fatal().Err(err).Msg("failure during post init migrations")
|
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,16 @@ package migrator
|
||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Migrator) migrateDBVersionToDB90() error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +44,7 @@ func (m *Migrator) updateEdgeStackStatusForDB90() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Migrator) updateUserThemForDB90() error {
|
func (m *Migrator) updateUserThemeForDB90() error {
|
||||||
log.Info().Msg("updating existing user theme settings")
|
log.Info().Msg("updating existing user theme settings")
|
||||||
|
|
||||||
users, err := m.userService.Users()
|
users, err := m.userService.Users()
|
||||||
|
@ -60,3 +65,28 @@ func (m *Migrator) updateUserThemForDB90() error {
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
},
|
},
|
||||||
"EdgeCheckinInterval": 0,
|
"EdgeCheckinInterval": 0,
|
||||||
"EdgeKey": "",
|
"EdgeKey": "",
|
||||||
|
"EnableGPUManagement": false,
|
||||||
"Gpus": [],
|
"Gpus": [],
|
||||||
"GroupId": 1,
|
"GroupId": 1,
|
||||||
"Id": 1,
|
"Id": 1,
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
"LastCheckInDate": 0,
|
"LastCheckInDate": 0,
|
||||||
"Name": "local",
|
"Name": "local",
|
||||||
"PostInitMigrations": {
|
"PostInitMigrations": {
|
||||||
|
"MigrateGPUs": true,
|
||||||
"MigrateIngresses": true
|
"MigrateIngresses": true
|
||||||
},
|
},
|
||||||
"PublicURL": "",
|
"PublicURL": "",
|
||||||
|
|
|
@ -28,6 +28,10 @@ type endpointSettingsUpdatePayload struct {
|
||||||
AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"`
|
AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"`
|
||||||
// Whether host management features are enabled
|
// Whether host management features are enabled
|
||||||
EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"`
|
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 {
|
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
|
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.EnableGPUManagement != nil {
|
||||||
|
endpoint.EnableGPUManagement = *payload.EnableGPUManagement
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Gpus != nil {
|
||||||
|
endpoint.Gpus = payload.Gpus
|
||||||
|
}
|
||||||
|
|
||||||
endpoint.SecuritySettings = securitySettings
|
endpoint.SecuritySettings = securitySettings
|
||||||
|
|
||||||
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
@ -221,27 +220,7 @@ func buildLocalClient() (*kubernetes.Clientset, error) {
|
||||||
return kubernetes.NewForConfig(config)
|
return kubernetes.NewForConfig(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *ClientFactory) PostInitMigrateIngresses() error {
|
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint) 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 {
|
|
||||||
// classes is a list of controllers which have been manually added to the
|
// 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
|
// cluster setup view. These need to all be allowed globally, but then
|
||||||
// blocked in specific namespaces which they were not previously allowed in.
|
// blocked in specific namespaces which they were not previously allowed in.
|
||||||
|
|
|
@ -402,6 +402,8 @@ type (
|
||||||
Version string `example:"1.0.0"`
|
Version string `example:"1.0.0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnableGPUManagement bool `json:"EnableGPUManagement"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
TLS bool `json:"TLS,omitempty"`
|
TLS bool `json:"TLS,omitempty"`
|
||||||
|
@ -502,6 +504,7 @@ type (
|
||||||
// EndpointPostInitMigrations
|
// EndpointPostInitMigrations
|
||||||
EndpointPostInitMigrations struct {
|
EndpointPostInitMigrations struct {
|
||||||
MigrateIngresses bool `json:"MigrateIngresses"`
|
MigrateIngresses bool `json:"MigrateIngresses"`
|
||||||
|
MigrateGPUs bool `json:"MigrateGPUs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension represents a deprecated Portainer extension
|
// Extension represents a deprecated Portainer extension
|
||||||
|
|
|
@ -358,11 +358,6 @@ input:-webkit-autofill {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overide Vendor CSS */
|
/* Overide Vendor CSS */
|
||||||
|
|
||||||
.btn-link:hover {
|
|
||||||
color: var(--text-link-hover-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiSelect.inlineBlock button {
|
.multiSelect.inlineBlock button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
title="Search image on Docker Hub"
|
title="Search image on Docker Hub"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<pr-icon icon="'svg-docker'" size="'lg'"></pr-icon> Search
|
<pr-icon icon="'svg-docker'" size="'md'"></pr-icon> Search
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,5 +37,12 @@ export const componentsModule = angular
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'gpu',
|
'gpu',
|
||||||
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
|
r2a(Gpu, [
|
||||||
|
'values',
|
||||||
|
'onChange',
|
||||||
|
'gpus',
|
||||||
|
'usedGpus',
|
||||||
|
'usedAllGpus',
|
||||||
|
'enableGpuManagement',
|
||||||
|
])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -21,6 +21,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
'$timeout',
|
'$timeout',
|
||||||
'$transition$',
|
'$transition$',
|
||||||
'$filter',
|
'$filter',
|
||||||
|
'$analytics',
|
||||||
'Container',
|
'Container',
|
||||||
'ContainerHelper',
|
'ContainerHelper',
|
||||||
'Image',
|
'Image',
|
||||||
|
@ -35,6 +36,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
'FormValidator',
|
'FormValidator',
|
||||||
'RegistryService',
|
'RegistryService',
|
||||||
'SystemService',
|
'SystemService',
|
||||||
|
'SettingsService',
|
||||||
'PluginService',
|
'PluginService',
|
||||||
'HttpRequestHelper',
|
'HttpRequestHelper',
|
||||||
'endpoint',
|
'endpoint',
|
||||||
|
@ -46,6 +48,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$timeout,
|
$timeout,
|
||||||
$transition$,
|
$transition$,
|
||||||
$filter,
|
$filter,
|
||||||
|
$analytics,
|
||||||
Container,
|
Container,
|
||||||
ContainerHelper,
|
ContainerHelper,
|
||||||
Image,
|
Image,
|
||||||
|
@ -60,6 +63,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
FormValidator,
|
FormValidator,
|
||||||
RegistryService,
|
RegistryService,
|
||||||
SystemService,
|
SystemService,
|
||||||
|
SettingsService,
|
||||||
PluginService,
|
PluginService,
|
||||||
HttpRequestHelper,
|
HttpRequestHelper,
|
||||||
endpoint
|
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) {
|
function applyResourceControl(newContainer) {
|
||||||
const userId = Authentication.getUserDetails().ID;
|
const userId = Authentication.getUserDetails().ID;
|
||||||
const resourceControl = newContainer.Portainer.ResourceControl;
|
const resourceControl = newContainer.Portainer.ResourceControl;
|
||||||
|
@ -1101,7 +1117,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
return validateForm(accessControlData, $scope.isAdmin);
|
return validateForm(accessControlData, $scope.isAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSuccess() {
|
async function onSuccess() {
|
||||||
|
await sendAnalytics();
|
||||||
Notifications.success('Success', 'Container successfully created');
|
Notifications.success('Success', 'Container successfully created');
|
||||||
$state.go('docker.containers', {}, { reload: true });
|
$state.go('docker.containers', {}, { reload: true });
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
<form class="form-horizontal" autocomplete="off">
|
<form class="form-horizontal" autocomplete="off">
|
||||||
<!-- name-input -->
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-8">
|
||||||
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer" />
|
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,8 +37,6 @@
|
||||||
model="formValues.RegistryModel"
|
model="formValues.RegistryModel"
|
||||||
ng-if="formValues.RegistryModel.Registry"
|
ng-if="formValues.RegistryModel.Registry"
|
||||||
auto-complete="true"
|
auto-complete="true"
|
||||||
label-class="col-sm-1"
|
|
||||||
input-class="col-sm-11"
|
|
||||||
endpoint="endpoint"
|
endpoint="endpoint"
|
||||||
is-admin="isAdmin"
|
is-admin="isAdmin"
|
||||||
check-rate-limits="formValues.alwaysPull"
|
check-rate-limits="formValues.alwaysPull"
|
||||||
|
@ -169,7 +167,7 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm !ml-0"
|
||||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)
|
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)
|
||||||
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
||||||
ng-click="create()"
|
ng-click="create()"
|
||||||
|
@ -701,17 +699,20 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !shm-size-input -->
|
<!-- !shm-size-input -->
|
||||||
<!-- #region GPU -->
|
<!-- #region GPU -->
|
||||||
<div class="col-sm-12 form-section-title"> GPU </div>
|
<div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||||
|
<div class="col-sm-12 form-section-title"> GPU </div>
|
||||||
|
|
||||||
<gpu
|
<gpu
|
||||||
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
||||||
values="formValues.GPU"
|
values="formValues.GPU"
|
||||||
on-change="(onGpuChange)"
|
on-change="(onGpuChange)"
|
||||||
gpus="endpoint.Gpus"
|
gpus="endpoint.Gpus"
|
||||||
used-gpus="gpuUseList"
|
used-gpus="gpuUseList"
|
||||||
used-all-gpus="gpuUseAll"
|
used-all-gpus="gpuUseAll"
|
||||||
>
|
enable-gpu-management="endpoint.EnableGPUManagement"
|
||||||
</gpu>
|
>
|
||||||
|
</gpu>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- #endregion GPU -->
|
<!-- #endregion GPU -->
|
||||||
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
||||||
|
|
|
@ -106,7 +106,12 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<dashboard-item icon="'cpu'" type="'GPU'" value="endpoint.Gpus.length"></dashboard-item>
|
<dashboard-item
|
||||||
|
ng-if="endpoint.EnableGPUManagement && applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'"
|
||||||
|
icon="'cpu'"
|
||||||
|
type="'GPU'"
|
||||||
|
value="endpoint.Gpus.length"
|
||||||
|
></dashboard-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,10 +2,13 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
|
||||||
export default class DockerFeaturesConfigurationController {
|
export default class DockerFeaturesConfigurationController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $scope, EndpointService, Notifications, StateManager) {
|
constructor($async, $scope, $state, $analytics, EndpointService, SettingsService, Notifications, StateManager) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$scope = $scope;
|
this.$scope = $scope;
|
||||||
|
this.$state = $state;
|
||||||
|
this.$analytics = $analytics;
|
||||||
this.EndpointService = EndpointService;
|
this.EndpointService = EndpointService;
|
||||||
|
this.SettingsService = SettingsService;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.StateManager = StateManager;
|
this.StateManager = StateManager;
|
||||||
|
|
||||||
|
@ -35,6 +38,8 @@ export default class DockerFeaturesConfigurationController {
|
||||||
this.save = this.save.bind(this);
|
this.save = this.save.bind(this);
|
||||||
this.onChangeField = this.onChangeField.bind(this);
|
this.onChangeField = this.onChangeField.bind(this);
|
||||||
this.onToggleAutoUpdate = this.onToggleAutoUpdate.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.onChangeEnableHostManagementFeatures = this.onChangeField('enableHostManagementFeatures');
|
||||||
this.onChangeAllowVolumeBrowserForRegularUsers = this.onChangeField('allowVolumeBrowserForRegularUsers');
|
this.onChangeAllowVolumeBrowserForRegularUsers = this.onChangeField('allowVolumeBrowserForRegularUsers');
|
||||||
this.onChangeDisableBindMountsForRegularUsers = this.onChangeField('disableBindMountsForRegularUsers');
|
this.onChangeDisableBindMountsForRegularUsers = this.onChangeField('disableBindMountsForRegularUsers');
|
||||||
|
@ -52,6 +57,12 @@ export default class DockerFeaturesConfigurationController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleGPUManagement(checked) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.state.enableGPUManagement = checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onChange(values) {
|
onChange(values) {
|
||||||
return this.$scope.$evalAsync(() => {
|
return this.$scope.$evalAsync(() => {
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
|
@ -69,6 +80,12 @@ export default class DockerFeaturesConfigurationController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onGpusChange(value) {
|
||||||
|
return this.$async(async () => {
|
||||||
|
this.endpoint.Gpus = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isContainerEditDisabled() {
|
isContainerEditDisabled() {
|
||||||
const {
|
const {
|
||||||
disableBindMountsForRegularUsers,
|
disableBindMountsForRegularUsers,
|
||||||
|
@ -92,7 +109,11 @@ export default class DockerFeaturesConfigurationController {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
this.state.actionInProgress = true;
|
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,
|
enableHostManagementFeatures: this.formValues.enableHostManagementFeatures,
|
||||||
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
|
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
|
||||||
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
|
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
|
||||||
|
@ -102,33 +123,53 @@ export default class DockerFeaturesConfigurationController {
|
||||||
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
|
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
|
||||||
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
|
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
|
||||||
allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers,
|
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');
|
this.Notifications.success('Success', 'Saved settings successfully');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.Notifications.error('Failure', e, 'Failed saving settings');
|
this.Notifications.error('Failure', e, 'Failed saving settings');
|
||||||
}
|
}
|
||||||
this.state.actionInProgress = false;
|
this.state.actionInProgress = false;
|
||||||
|
this.$state.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAgent() {
|
|
||||||
const applicationState = this.StateManager.getState();
|
|
||||||
return applicationState.endpoint.mode.agentProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
const securitySettings = this.endpoint.SecuritySettings;
|
const securitySettings = this.endpoint.SecuritySettings;
|
||||||
|
|
||||||
const isAgent = this.checkAgent();
|
const applicationState = this.StateManager.getState();
|
||||||
this.isAgent = isAgent;
|
this.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||||
|
|
||||||
|
this.isDockerStandaloneEnv = applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE';
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures,
|
enableHostManagementFeatures: this.isAgent && securitySettings.enableHostManagementFeatures,
|
||||||
allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
|
allowVolumeBrowserForRegularUsers: this.isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
|
||||||
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
|
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
|
||||||
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
|
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
|
||||||
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
|
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
|
||||||
|
@ -137,5 +178,9 @@ export default class DockerFeaturesConfigurationController {
|
||||||
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
|
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
|
||||||
disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers,
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,9 +150,42 @@
|
||||||
<!-- other -->
|
<!-- other -->
|
||||||
<div class="col-sm-12 form-section-title"> Other </div>
|
<div class="col-sm-12 form-section-title"> Other </div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12 pb-3">
|
||||||
|
<insights-box
|
||||||
|
header="'GPU settings update'"
|
||||||
|
set-html-content="true"
|
||||||
|
insight-close-id="'gpu-settings-update-closed'"
|
||||||
|
content="'
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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).
|
||||||
|
</p>'"
|
||||||
|
></insights-box>
|
||||||
|
</div>
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
label="'Show a notification to indicate out-of-date images for Docker environments'"
|
label="'Show GPU in the UI'"
|
||||||
|
tooltip="'This allows managing of GPUs for container/stack hardware acceleration via the Portainer UI.'"
|
||||||
|
checked="$ctrl.state.enableGPUManagement"
|
||||||
|
name="'enableGPUManagement'"
|
||||||
|
on-change="($ctrl.onToggleGPUManagement)"
|
||||||
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
disabled="!$ctrl.isDockerStandaloneEnv"
|
||||||
|
></por-switch-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="pl-4">
|
||||||
|
<gpus-list ng-if="$ctrl.state.enableGPUManagement && $ctrl.endpoint" value="$ctrl.endpoint.Gpus" on-change="($ctrl.onGpusChange)"></gpus-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<por-switch-field
|
||||||
|
label="'Show an image(s) up to date indicator for Stacks, Services and Containers'"
|
||||||
checked="false"
|
checked="false"
|
||||||
name="'outOfDateImageToggle'"
|
name="'outOfDateImageToggle'"
|
||||||
label-class="'col-sm-7 col-lg-4'"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
@ -166,7 +199,13 @@
|
||||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.save()" ng-disabled="$ctrl.state.actionInProgress" button-spinner="$ctrl.state.actionInProgress">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm !ml-0"
|
||||||
|
ng-click="$ctrl.save()"
|
||||||
|
ng-disabled="$ctrl.state.actionInProgress"
|
||||||
|
button-spinner="$ctrl.state.actionInProgress"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
|
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
|
||||||
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
|
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { r2a } from '@/react-tools/react2angular';
|
||||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||||
import { EdgeKeyDisplay } from '@/react/portainer/environments/ItemView/EdgeKeyDisplay';
|
import { EdgeKeyDisplay } from '@/react/portainer/environments/ItemView/EdgeKeyDisplay';
|
||||||
import { KVMControl } from '@/react/portainer/environments/KvmView/KVMControl';
|
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
|
export const environmentsModule = angular
|
||||||
.module('portainer.app.react.components.environments', [])
|
.module('portainer.app.react.components.environments', [])
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { Slider } from '@@/form-components/Slider';
|
||||||
import { TagButton } from '@@/TagButton';
|
import { TagButton } from '@@/TagButton';
|
||||||
import { BETeaserButton } from '@@/BETeaserButton';
|
import { BETeaserButton } from '@@/BETeaserButton';
|
||||||
import { CodeEditor } from '@@/CodeEditor';
|
import { CodeEditor } from '@@/CodeEditor';
|
||||||
|
import { InsightsBox } from '@@/InsightsBox';
|
||||||
|
|
||||||
import { fileUploadField } from './file-upload-field';
|
import { fileUploadField } from './file-upload-field';
|
||||||
import { switchField } from './switch-field';
|
import { switchField } from './switch-field';
|
||||||
|
@ -32,6 +33,7 @@ import { customTemplatesModule } from './custom-templates';
|
||||||
import { gitFormModule } from './git-form';
|
import { gitFormModule } from './git-form';
|
||||||
import { settingsModule } from './settings';
|
import { settingsModule } from './settings';
|
||||||
import { accessControlModule } from './access-control';
|
import { accessControlModule } from './access-control';
|
||||||
|
import { environmentsModule } from './envronments';
|
||||||
import { envListModule } from './enviroments-list-view-components';
|
import { envListModule } from './enviroments-list-view-components';
|
||||||
|
|
||||||
export const componentsModule = angular
|
export const componentsModule = angular
|
||||||
|
@ -40,6 +42,7 @@ export const componentsModule = angular
|
||||||
gitFormModule,
|
gitFormModule,
|
||||||
settingsModule,
|
settingsModule,
|
||||||
accessControlModule,
|
accessControlModule,
|
||||||
|
environmentsModule,
|
||||||
envListModule,
|
envListModule,
|
||||||
])
|
])
|
||||||
.component(
|
.component(
|
||||||
|
@ -74,6 +77,10 @@ export const componentsModule = angular
|
||||||
.component('badge', r2a(Badge, ['type', 'className']))
|
.component('badge', r2a(Badge, ['type', 'className']))
|
||||||
.component('fileUploadField', fileUploadField)
|
.component('fileUploadField', fileUploadField)
|
||||||
.component('porSwitchField', switchField)
|
.component('porSwitchField', switchField)
|
||||||
|
.component(
|
||||||
|
'insightsBox',
|
||||||
|
r2a(InsightsBox, ['header', 'content', 'setHtmlContent', 'insightCloseId'])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'passwordCheckHint',
|
'passwordCheckHint',
|
||||||
r2a(withReactQuery(PasswordCheckHint), [
|
r2a(withReactQuery(PasswordCheckHint), [
|
||||||
|
|
|
@ -36,7 +36,6 @@ angular.module('portainer.app').factory('Authentication', [
|
||||||
await setUser(jwt);
|
await setUser(jwt);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Unable to initialize authentication service', error);
|
|
||||||
return tryAutoLoginExtension();
|
return tryAutoLoginExtension();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,14 +210,30 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !open-amt info -->
|
<!-- !open-amt info -->
|
||||||
<!-- gpus info -->
|
<!-- gpus info -->
|
||||||
<div class="col-sm-12 form-section-title">Hardware acceleration</div>
|
<div class="mb-4">
|
||||||
<gpus-list ng-if="endpoint && endpoint.Gpus" value="endpoint.Gpus" on-change="(onGpusChange)"></gpus-list>
|
<insights-box
|
||||||
|
ng-if="isDockerStandaloneEnv"
|
||||||
|
header="'GPU settings update'"
|
||||||
|
set-html-content="true"
|
||||||
|
insight-close-id="'gpu-settings-update-closed'"
|
||||||
|
content="'
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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).
|
||||||
|
</p>'"
|
||||||
|
></insights-box>
|
||||||
|
</div>
|
||||||
<!-- gpus info -->
|
<!-- gpus info -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm !ml-0"
|
||||||
ng-disabled="state.actionInProgress || !endpoint.Name || !endpoint.URL || (endpoint.TLS && ((endpoint.TLSVerify && !formValues.TLSCACert) || (endpoint.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))"
|
ng-disabled="state.actionInProgress || !endpoint.Name || !endpoint.URL || (endpoint.TLS && ((endpoint.TLSVerify && !formValues.TLSCACert) || (endpoint.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))"
|
||||||
ng-click="updateEndpoint()"
|
ng-click="updateEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
|
|
|
@ -9,11 +9,10 @@ import { confirmDestructive } from '@@/modals/confirm';
|
||||||
import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
|
import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
|
||||||
|
|
||||||
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||||
import { GpusListAngular } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
|
||||||
import { confirmDisassociate } from '@/react/portainer/environments/ItemView/ConfirmDisassociateModel';
|
import { confirmDisassociate } from '@/react/portainer/environments/ItemView/ConfirmDisassociateModel';
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
import { buildConfirmButton } from '@@/modals/utils';
|
||||||
|
|
||||||
angular.module('portainer.app').component('gpusList', GpusListAngular).controller('EndpointController', EndpointController);
|
angular.module('portainer.app').controller('EndpointController', EndpointController);
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
function EndpointController(
|
function EndpointController(
|
||||||
|
@ -25,6 +24,7 @@ function EndpointController(
|
||||||
clipboard,
|
clipboard,
|
||||||
EndpointService,
|
EndpointService,
|
||||||
GroupService,
|
GroupService,
|
||||||
|
StateManager,
|
||||||
|
|
||||||
Notifications,
|
Notifications,
|
||||||
Authentication,
|
Authentication,
|
||||||
|
@ -143,8 +143,6 @@ function EndpointController(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.onGpusChange = onGpusChange;
|
|
||||||
|
|
||||||
Array.prototype.indexOf = function (val) {
|
Array.prototype.indexOf = function (val) {
|
||||||
for (var i = 0; i < this.length; i++) {
|
for (var i = 0; i < this.length; i++) {
|
||||||
if (this[i] == val) return i;
|
if (this[i] == val) return i;
|
||||||
|
@ -158,21 +156,6 @@ function EndpointController(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function onGpusChange(value) {
|
|
||||||
return $async(async () => {
|
|
||||||
$scope.endpoint.Gpus = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyGpus() {
|
|
||||||
var i = ($scope.endpoint.Gpus || []).length;
|
|
||||||
while (i--) {
|
|
||||||
if ($scope.endpoint.Gpus[i].name === '' || $scope.endpoint.Gpus[i].name === null) {
|
|
||||||
$scope.endpoint.Gpus.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.updateEndpoint = async function () {
|
$scope.updateEndpoint = async function () {
|
||||||
var endpoint = $scope.endpoint;
|
var endpoint = $scope.endpoint;
|
||||||
var securityData = $scope.formValues.SecurityFormData;
|
var securityData = $scope.formValues.SecurityFormData;
|
||||||
|
@ -193,7 +176,6 @@ function EndpointController(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyGpus();
|
|
||||||
var payload = {
|
var payload = {
|
||||||
Name: endpoint.Name,
|
Name: endpoint.Name,
|
||||||
PublicURL: endpoint.PublicURL,
|
PublicURL: endpoint.PublicURL,
|
||||||
|
@ -290,6 +272,9 @@ function EndpointController(
|
||||||
try {
|
try {
|
||||||
const [endpoint, groups, settings] = await Promise.all([EndpointService.endpoint($transition$.params().id), GroupService.groups(), SettingsService.settings()]);
|
const [endpoint, groups, settings] = await Promise.all([EndpointService.endpoint($transition$.params().id), GroupService.groups(), SettingsService.settings()]);
|
||||||
|
|
||||||
|
const applicationState = StateManager.getState();
|
||||||
|
$scope.isDockerStandaloneEnv = applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE';
|
||||||
|
|
||||||
if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) {
|
if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) {
|
||||||
$scope.endpointType = 'local';
|
$scope.endpointType = 'local';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -83,6 +83,7 @@ export function createMockEnvironment(): Environment {
|
||||||
},
|
},
|
||||||
Nomad: { Snapshots: [] },
|
Nomad: { Snapshots: [] },
|
||||||
EdgeKey: '',
|
EdgeKey: '',
|
||||||
|
EnableGPUManagement: false,
|
||||||
Id: 3,
|
Id: 3,
|
||||||
UserTrusted: false,
|
UserTrusted: false,
|
||||||
Edge: {
|
Edge: {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
|
||||||
|
import { InsightsBox, Props } from './InsightsBox';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: InsightsBox,
|
||||||
|
header: 'Components/InsightsBox',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
function Template({ header, content }: Props) {
|
||||||
|
return <InsightsBox header={header} content={content} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<Props> = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
header: 'Insights box header',
|
||||||
|
content: 'This is the content of the insights box',
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { InsightsBox } from './InsightsBox';
|
||||||
|
|
||||||
|
test('should display a InsightsBox with a header and content', async () => {
|
||||||
|
const header = 'test header';
|
||||||
|
const content = 'test content';
|
||||||
|
const { findByText } = render(
|
||||||
|
<InsightsBox header={header} content={content} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerFound = await findByText(header);
|
||||||
|
expect(headerFound).toBeTruthy();
|
||||||
|
const contentFound = await findByText(content);
|
||||||
|
expect(contentFound).toBeTruthy();
|
||||||
|
});
|
|
@ -0,0 +1,63 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { Lightbulb, X } from 'lucide-react';
|
||||||
|
import { ReactNode, useMemo } from 'react';
|
||||||
|
import sanitize from 'sanitize-html';
|
||||||
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
|
||||||
|
import { insightStore } from './insights-store';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
header: string;
|
||||||
|
content: ReactNode;
|
||||||
|
setHtmlContent?: boolean;
|
||||||
|
insightCloseId?: string; // set if you want to be able to close the box and not show it again
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InsightsBox({
|
||||||
|
header,
|
||||||
|
content,
|
||||||
|
setHtmlContent,
|
||||||
|
insightCloseId,
|
||||||
|
}: Props) {
|
||||||
|
// allow to close the box and not show it again in local storage with zustand
|
||||||
|
const { addInsightIDClosed, isClosed } = useStore(insightStore);
|
||||||
|
const isInsightClosed = isClosed(insightCloseId);
|
||||||
|
|
||||||
|
// allow angular views to set html messages for the insights box
|
||||||
|
const htmlContent = useMemo(() => {
|
||||||
|
if (setHtmlContent && typeof content === 'string') {
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: sanitize(content) }} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [setHtmlContent, content]);
|
||||||
|
|
||||||
|
if (isInsightClosed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex w-full gap-1 rounded-lg bg-gray-modern-3 p-4 text-sm th-highcontrast:bg-legacy-grey-3 th-dark:bg-legacy-grey-3">
|
||||||
|
<div className="shrink-0">
|
||||||
|
<Lightbulb className="h-4 text-warning-7 th-highcontrast:text-warning-6 th-dark:text-warning-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className={clsx('mb-2 font-bold', insightCloseId && 'pr-4')}>
|
||||||
|
{header}
|
||||||
|
</p>
|
||||||
|
<div>{htmlContent || content}</div>
|
||||||
|
</div>
|
||||||
|
{insightCloseId && (
|
||||||
|
<Button
|
||||||
|
icon={X}
|
||||||
|
className="absolute top-2 right-2 flex !text-gray-7 hover:!text-gray-8 th-highcontrast:!text-gray-6 th-highcontrast:hover:!text-gray-5 th-dark:!text-gray-6 th-dark:hover:!text-gray-5"
|
||||||
|
color="link"
|
||||||
|
size="medium"
|
||||||
|
onClick={() => addInsightIDClosed(insightCloseId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { InsightsBox } from './InsightsBox';
|
|
@ -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<InsightsStore>()(
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
|
@ -1,7 +1,3 @@
|
||||||
.items {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items > * + * {
|
.items > * + * {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
@ -24,11 +20,6 @@
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-actions {
|
|
||||||
display: flex;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-item {
|
.default-item {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { FormikErrors } from 'formik';
|
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 { Tooltip } from '@@/Tip/Tooltip';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
|
@ -79,18 +79,10 @@ export function InputList<T = DefaultType>({
|
||||||
return (
|
return (
|
||||||
<div className={clsx('form-group', styles.root)}>
|
<div className={clsx('form-group', styles.root)}>
|
||||||
<div className={clsx('col-sm-12', styles.header)}>
|
<div className={clsx('col-sm-12', styles.header)}>
|
||||||
<div className={clsx('control-label text-left', styles.label)}>
|
<span className="control-label space-right pt-2 text-left !font-bold">
|
||||||
{label}
|
{label}
|
||||||
{tooltip && <Tooltip message={tooltip} />}
|
{tooltip && <Tooltip message={tooltip} />}
|
||||||
</div>
|
</span>
|
||||||
{!(isAddButtonHidden || readOnly) && (
|
|
||||||
<AddButton
|
|
||||||
label={addLabel}
|
|
||||||
className="space-left"
|
|
||||||
onClick={handleAdd}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{textTip && (
|
{textTip && (
|
||||||
|
@ -99,68 +91,86 @@ export function InputList<T = DefaultType>({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={clsx('col-sm-12', styles.items, 'space-y-4')}>
|
{value.length > 0 && (
|
||||||
{value.map((item, index) => {
|
<div className="col-sm-12 mt-5 flex flex-col gap-y-5">
|
||||||
const key = itemKeyGetter(item, index);
|
{value.map((item, index) => {
|
||||||
const error = typeof errors === 'object' ? errors[index] : undefined;
|
const key = itemKeyGetter(item, index);
|
||||||
|
const error =
|
||||||
|
typeof errors === 'object' ? errors[index] : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.itemLine,
|
styles.itemLine,
|
||||||
{ [styles.hasError]: !!error },
|
{ [styles.hasError]: !!error },
|
||||||
'vertical-center'
|
'vertical-center'
|
||||||
)}
|
|
||||||
>
|
|
||||||
{Item ? (
|
|
||||||
<Item
|
|
||||||
item={item}
|
|
||||||
onChange={(value: T) => handleChangeItem(key, value)}
|
|
||||||
error={error}
|
|
||||||
disabled={disabled}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
renderItem(
|
|
||||||
item,
|
|
||||||
(value: T) => handleChangeItem(key, value),
|
|
||||||
error
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div className={clsx(styles.itemActions, 'items-start')}>
|
|
||||||
{!readOnly && movable && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="medium"
|
|
||||||
disabled={disabled || index === 0}
|
|
||||||
onClick={() => handleMoveUp(index)}
|
|
||||||
className="vertical-center btn-only-icon"
|
|
||||||
icon={ArrowUp}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="medium"
|
|
||||||
type="button"
|
|
||||||
disabled={disabled || index === value.length - 1}
|
|
||||||
onClick={() => handleMoveDown(index)}
|
|
||||||
className="vertical-center btn-only-icon"
|
|
||||||
icon={ArrowDown}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{!readOnly && (
|
>
|
||||||
<Button
|
{Item ? (
|
||||||
color="dangerlight"
|
<Item
|
||||||
size="medium"
|
item={item}
|
||||||
onClick={() => handleRemoveItem(key, item)}
|
onChange={(value: T) => handleChangeItem(key, value)}
|
||||||
className="vertical-center btn-only-icon"
|
error={error}
|
||||||
icon={Trash2}
|
disabled={disabled}
|
||||||
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
renderItem(
|
||||||
|
item,
|
||||||
|
(value: T) => handleChangeItem(key, value),
|
||||||
|
error
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
<div className="items-start">
|
||||||
|
{!readOnly && movable && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="medium"
|
||||||
|
disabled={disabled || index === 0}
|
||||||
|
onClick={() => handleMoveUp(index)}
|
||||||
|
className="vertical-center btn-only-icon"
|
||||||
|
icon={ArrowUp}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="medium"
|
||||||
|
type="button"
|
||||||
|
disabled={disabled || index === value.length - 1}
|
||||||
|
onClick={() => handleMoveDown(index)}
|
||||||
|
className="vertical-center btn-only-icon"
|
||||||
|
icon={ArrowDown}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!readOnly && (
|
||||||
|
<Button
|
||||||
|
color="dangerlight"
|
||||||
|
size="medium"
|
||||||
|
onClick={() => handleRemoveItem(key, item)}
|
||||||
|
className="vertical-center btn-only-icon"
|
||||||
|
icon={Trash2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="col-sm-12 mt-5">
|
||||||
|
{!(isAddButtonHidden || readOnly) && (
|
||||||
|
<Button
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={disabled}
|
||||||
|
type="button"
|
||||||
|
color="default"
|
||||||
|
className="!ml-0"
|
||||||
|
size="small"
|
||||||
|
icon={Plus}
|
||||||
|
>
|
||||||
|
{addLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { OptionProps } from 'react-select/dist/declarations/src/components/Optio
|
||||||
import { Select } from '@@/form-components/ReactSelect';
|
import { Select } from '@@/form-components/ReactSelect';
|
||||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||||
import { Tooltip } from '@@/Tip/Tooltip';
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
@ -35,6 +36,7 @@ export interface Props {
|
||||||
gpus: GPU[];
|
gpus: GPU[];
|
||||||
usedGpus: string[];
|
usedGpus: string[];
|
||||||
usedAllGpus: boolean;
|
usedAllGpus: boolean;
|
||||||
|
enableGpuManagement?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NvidiaCapabilitiesOptions = [
|
const NvidiaCapabilitiesOptions = [
|
||||||
|
@ -103,6 +105,7 @@ export function Gpu({
|
||||||
gpus = [],
|
gpus = [],
|
||||||
usedGpus = [],
|
usedGpus = [],
|
||||||
usedAllGpus,
|
usedAllGpus,
|
||||||
|
enableGpuManagement,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const options = (gpus || []).map((gpu) => ({
|
const options = (gpus || []).map((gpu) => ({
|
||||||
|
@ -181,30 +184,38 @@ export function Gpu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{!enableGpuManagement && (
|
||||||
|
<TextTip color="blue">
|
||||||
|
GPU in the UI is not currently enabled for this environment.
|
||||||
|
</TextTip>
|
||||||
|
)}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-3 col-lg-2 control-label text-left">
|
<div className="col-sm-3 col-lg-2 control-label text-left">
|
||||||
Enable GPU
|
Enable GPU
|
||||||
<Switch
|
<Switch
|
||||||
id="enabled"
|
id="enabled"
|
||||||
name="enabled"
|
name="enabled"
|
||||||
checked={values.enabled}
|
checked={values.enabled && !!enableGpuManagement}
|
||||||
onChange={toggleEnableGpu}
|
onChange={toggleEnableGpu}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
|
disabled={enableGpuManagement === false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-9 col-lg-10 text-left">
|
{enableGpuManagement && values.enabled && (
|
||||||
<Select<GpuOption, true>
|
<div className="col-sm-9 col-lg-10 text-left">
|
||||||
isMulti
|
<Select<GpuOption, true>
|
||||||
closeMenuOnSelect
|
isMulti
|
||||||
value={gpuValue}
|
closeMenuOnSelect
|
||||||
isClearable={false}
|
value={gpuValue}
|
||||||
backspaceRemovesValue={false}
|
isClearable={false}
|
||||||
isDisabled={!values.enabled}
|
backspaceRemovesValue={false}
|
||||||
onChange={onChangeSelectedGpus}
|
isDisabled={!values.enabled}
|
||||||
options={options}
|
onChange={onChangeSelectedGpus}
|
||||||
components={{ MultiValueRemove }}
|
options={options}
|
||||||
/>
|
components={{ MultiValueRemove }}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{values.enabled && (
|
{values.enabled && (
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { gpus } from './gpus';
|
||||||
|
|
||||||
export function useColumns(
|
export function useColumns(
|
||||||
isHostColumnVisible: boolean,
|
isHostColumnVisible: boolean,
|
||||||
isGPUsColumnVisible: boolean
|
isGPUsColumnVisible?: boolean
|
||||||
) {
|
) {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import _ from 'lodash';
|
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 { 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 { DockerContainer, ContainerStatus } from './types';
|
||||||
import { DockerContainerResponse } from './types/response';
|
import { DockerContainerResponse } from './types/response';
|
||||||
|
@ -95,10 +96,13 @@ function createStatus(statusText = ''): ContainerStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShowGPUsColumn(environmentID: EnvironmentId) {
|
export function useShowGPUsColumn(environmentID: EnvironmentId) {
|
||||||
const envInfoQuery = useInfo(
|
const isDockerStandaloneQuery = useInfo(
|
||||||
environmentID,
|
environmentID,
|
||||||
(info) => !!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable
|
(info) => !(!!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable) // is not a swarm environment, therefore docker standalone
|
||||||
);
|
);
|
||||||
|
const enableGPUManagementQuery = useEnvironment(
|
||||||
return envInfoQuery.data !== true && !envInfoQuery.isLoading;
|
environmentID,
|
||||||
|
(env) => env?.EnableGPUManagement
|
||||||
|
);
|
||||||
|
return isDockerStandaloneQuery.data && enableGPUManagementQuery.data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,12 @@ function Item({ item, onChange }: ItemProps<Gpu>) {
|
||||||
export function GpusList({ value, onChange }: Props) {
|
export function GpusList({ value, onChange }: Props) {
|
||||||
return (
|
return (
|
||||||
<InputList<Gpu>
|
<InputList<Gpu>
|
||||||
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}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
itemBuilder={() => ({ value: '', name: '' })}
|
itemBuilder={() => ({ value: '', name: '' })}
|
||||||
addLabel="add"
|
addLabel="Add GPU"
|
||||||
item={Item}
|
item={Item}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
|
@ -1,4 +1,3 @@
|
||||||
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
||||||
import { type TagId } from '@/portainer/tags/types';
|
import { type TagId } from '@/portainer/tags/types';
|
||||||
|
@ -18,7 +17,6 @@ interface CreateLocalDockerEnvironment {
|
||||||
socketPath?: string;
|
socketPath?: string;
|
||||||
publicUrl?: string;
|
publicUrl?: string;
|
||||||
meta?: EnvironmentMetadata;
|
meta?: EnvironmentMetadata;
|
||||||
gpus?: Gpu[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createLocalDockerEnvironment({
|
export async function createLocalDockerEnvironment({
|
||||||
|
@ -26,7 +24,6 @@ export async function createLocalDockerEnvironment({
|
||||||
socketPath = '',
|
socketPath = '',
|
||||||
publicUrl = '',
|
publicUrl = '',
|
||||||
meta = { tagIds: [] },
|
meta = { tagIds: [] },
|
||||||
gpus = [],
|
|
||||||
}: CreateLocalDockerEnvironment) {
|
}: CreateLocalDockerEnvironment) {
|
||||||
const url = prefixPath(socketPath);
|
const url = prefixPath(socketPath);
|
||||||
|
|
||||||
|
@ -37,7 +34,6 @@ export async function createLocalDockerEnvironment({
|
||||||
url,
|
url,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
meta,
|
meta,
|
||||||
gpus,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -113,7 +109,6 @@ export interface EnvironmentOptions {
|
||||||
azure?: AzureSettings;
|
azure?: AzureSettings;
|
||||||
tls?: TLSSettings;
|
tls?: TLSSettings;
|
||||||
isEdgeDevice?: boolean;
|
isEdgeDevice?: boolean;
|
||||||
gpus?: Gpu[];
|
|
||||||
pollFrequency?: number;
|
pollFrequency?: number;
|
||||||
edge?: EdgeSettings;
|
edge?: EdgeSettings;
|
||||||
tunnelServerAddr?: string;
|
tunnelServerAddr?: string;
|
||||||
|
@ -145,7 +140,6 @@ export interface CreateAgentEnvironmentValues {
|
||||||
name: string;
|
name: string;
|
||||||
environmentUrl: string;
|
environmentUrl: string;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
gpus: Gpu[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAgentEnvironment({
|
export function createAgentEnvironment({
|
||||||
|
@ -173,7 +167,6 @@ interface CreateEdgeAgentEnvironment {
|
||||||
tunnelServerAddr?: string;
|
tunnelServerAddr?: string;
|
||||||
meta?: EnvironmentMetadata;
|
meta?: EnvironmentMetadata;
|
||||||
pollFrequency: number;
|
pollFrequency: number;
|
||||||
gpus?: Gpu[];
|
|
||||||
isEdgeDevice?: boolean;
|
isEdgeDevice?: boolean;
|
||||||
edge: EdgeSettings;
|
edge: EdgeSettings;
|
||||||
}
|
}
|
||||||
|
@ -182,7 +175,6 @@ export function createEdgeAgentEnvironment({
|
||||||
name,
|
name,
|
||||||
portainerUrl,
|
portainerUrl,
|
||||||
meta = { tagIds: [] },
|
meta = { tagIds: [] },
|
||||||
gpus = [],
|
|
||||||
isEdgeDevice,
|
isEdgeDevice,
|
||||||
pollFrequency,
|
pollFrequency,
|
||||||
edge,
|
edge,
|
||||||
|
@ -196,7 +188,6 @@ export function createEdgeAgentEnvironment({
|
||||||
skipVerify: true,
|
skipVerify: true,
|
||||||
skipClientVerify: true,
|
skipClientVerify: true,
|
||||||
},
|
},
|
||||||
gpus,
|
|
||||||
isEdgeDevice,
|
isEdgeDevice,
|
||||||
pollFrequency,
|
pollFrequency,
|
||||||
edge,
|
edge,
|
||||||
|
@ -226,7 +217,6 @@ async function createEnvironment(
|
||||||
TagIds: arrayToJson(tagIds),
|
TagIds: arrayToJson(tagIds),
|
||||||
CheckinInterval: options.pollFrequency,
|
CheckinInterval: options.pollFrequency,
|
||||||
IsEdgeDevice: options.isEdgeDevice,
|
IsEdgeDevice: options.isEdgeDevice,
|
||||||
Gpus: arrayToJson(options.gpus),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tls, azure } = options;
|
const { tls, azure } = options;
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
import { getEndpoint } from '@/react/portainer/environments/environment.service';
|
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';
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
export function useEnvironment(id?: EnvironmentId) {
|
export function useEnvironment<T = Environment | null>(
|
||||||
|
id?: EnvironmentId,
|
||||||
|
select?: (environment: Environment | null) => T
|
||||||
|
) {
|
||||||
return useQuery(['environments', id], () => (id ? getEndpoint(id) : null), {
|
return useQuery(['environments', id], () => (id ? getEndpoint(id) : null), {
|
||||||
|
select,
|
||||||
...withError('Failed loading environment'),
|
...withError('Failed loading environment'),
|
||||||
staleTime: 50,
|
staleTime: 50,
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
|
|
|
@ -131,6 +131,7 @@ export type Environment = {
|
||||||
TagIds: TagId[];
|
TagIds: TagId[];
|
||||||
GroupId: EnvironmentGroupId;
|
GroupId: EnvironmentGroupId;
|
||||||
DeploymentOptions: DeploymentOptions | null;
|
DeploymentOptions: DeploymentOptions | null;
|
||||||
|
EnableGPUManagement: boolean;
|
||||||
EdgeID?: string;
|
EdgeID?: string;
|
||||||
EdgeKey: string;
|
EdgeKey: string;
|
||||||
EdgeCheckinInterval?: number;
|
EdgeCheckinInterval?: number;
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useReducer } from 'react';
|
||||||
import { Plug2 } from 'lucide-react';
|
import { Plug2 } from 'lucide-react';
|
||||||
|
|
||||||
import { useCreateRemoteEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
|
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 { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import {
|
import {
|
||||||
Environment,
|
Environment,
|
||||||
|
@ -13,6 +12,7 @@ import {
|
||||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { Input } from '@@/form-components/Input';
|
import { Input } from '@@/form-components/Input';
|
||||||
|
import { InsightsBox } from '@@/InsightsBox';
|
||||||
|
|
||||||
import { NameField } from '../../shared/NameField';
|
import { NameField } from '../../shared/NameField';
|
||||||
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
|
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
|
||||||
|
@ -23,9 +23,10 @@ import { TLSFieldset } from './TLSFieldset';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
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 [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||||
const initialValues: FormValues = {
|
const initialValues: FormValues = {
|
||||||
url: '',
|
url: '',
|
||||||
|
@ -35,7 +36,6 @@ export function APIForm({ onCreate }: Props) {
|
||||||
groupId: 1,
|
groupId: 1,
|
||||||
tagIds: [],
|
tagIds: [],
|
||||||
},
|
},
|
||||||
gpus: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutation = useCreateRemoteEnvironmentMutation(
|
const mutation = useCreateRemoteEnvironmentMutation(
|
||||||
|
@ -73,7 +73,33 @@ export function APIForm({ onCreate }: Props) {
|
||||||
<TLSFieldset />
|
<TLSFieldset />
|
||||||
|
|
||||||
<MoreSettingsSection>
|
<MoreSettingsSection>
|
||||||
<Hardware />
|
{isDockerStandalone && (
|
||||||
|
<InsightsBox
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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).
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
header="GPU settings update"
|
||||||
|
insightCloseId="gpu-settings-update-closed"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MoreSettingsSection>
|
</MoreSettingsSection>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
@ -104,7 +130,6 @@ export function APIForm({ onCreate }: Props) {
|
||||||
options: {
|
options: {
|
||||||
tls,
|
tls,
|
||||||
meta: values.meta,
|
meta: values.meta,
|
||||||
gpus: values.gpus,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { boolean, object, SchemaOf, string } from 'yup';
|
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 { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
||||||
import { useNameValidation } from '../../shared/NameField';
|
import { useNameValidation } from '../../shared/NameField';
|
||||||
|
|
||||||
|
@ -16,6 +14,5 @@ export function useValidation(): SchemaOf<FormValues> {
|
||||||
skipVerify: boolean(),
|
skipVerify: boolean(),
|
||||||
meta: metadataValidation(),
|
meta: metadataValidation(),
|
||||||
...certsValidation(),
|
...certsValidation(),
|
||||||
gpus: gpusListValidation(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,13 @@ import { APIForm } from './APIForm';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
onCreate(environment: Environment): void;
|
||||||
|
isDockerStandalone?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function APITab({ onCreate }: Props) {
|
export function APITab({ onCreate, isDockerStandalone }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<APIForm onCreate={onCreate} />
|
<APIForm onCreate={onCreate} isDockerStandalone={isDockerStandalone} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
|
||||||
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
|
@ -10,5 +9,4 @@ export interface FormValues {
|
||||||
certFile?: File;
|
certFile?: File;
|
||||||
keyFile?: File;
|
keyFile?: File;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
gpus?: Gpu[];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useReducer } from 'react';
|
||||||
import { Plug2 } from 'lucide-react';
|
import { Plug2 } from 'lucide-react';
|
||||||
|
|
||||||
import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
|
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 { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
@ -11,6 +10,7 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { Input } from '@@/form-components/Input';
|
import { Input } from '@@/form-components/Input';
|
||||||
import { SwitchField } from '@@/form-components/SwitchField';
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
|
import { InsightsBox } from '@@/InsightsBox';
|
||||||
|
|
||||||
import { NameField } from '../../shared/NameField';
|
import { NameField } from '../../shared/NameField';
|
||||||
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
|
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
|
||||||
|
@ -20,16 +20,16 @@ import { FormValues } from './types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
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 [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||||
const initialValues: FormValues = {
|
const initialValues: FormValues = {
|
||||||
name: '',
|
name: '',
|
||||||
socketPath: '',
|
socketPath: '',
|
||||||
overridePath: false,
|
overridePath: false,
|
||||||
meta: { groupId: 1, tagIds: [] },
|
meta: { groupId: 1, tagIds: [] },
|
||||||
gpus: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutation = useCreateLocalDockerEnvironmentMutation();
|
const mutation = useCreateLocalDockerEnvironmentMutation();
|
||||||
|
@ -50,7 +50,33 @@ export function SocketForm({ onCreate }: Props) {
|
||||||
<OverrideSocketFieldset />
|
<OverrideSocketFieldset />
|
||||||
|
|
||||||
<MoreSettingsSection>
|
<MoreSettingsSection>
|
||||||
<Hardware />
|
{isDockerStandalone && (
|
||||||
|
<InsightsBox
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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).
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
header="GPU settings update"
|
||||||
|
insightCloseId="gpu-settings-update-closed"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MoreSettingsSection>
|
</MoreSettingsSection>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
@ -76,7 +102,6 @@ export function SocketForm({ onCreate }: Props) {
|
||||||
{
|
{
|
||||||
name: values.name,
|
name: values.name,
|
||||||
socketPath: values.overridePath ? values.socketPath : '',
|
socketPath: values.overridePath ? values.socketPath : '',
|
||||||
gpus: values.gpus,
|
|
||||||
meta: values.meta,
|
meta: values.meta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { boolean, object, SchemaOf, string } from 'yup';
|
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 { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
||||||
import { useNameValidation } from '../../shared/NameField';
|
import { useNameValidation } from '../../shared/NameField';
|
||||||
|
|
||||||
|
@ -21,6 +19,5 @@ export function useValidation(): SchemaOf<FormValues> {
|
||||||
)
|
)
|
||||||
: schema
|
: schema
|
||||||
),
|
),
|
||||||
gpus: gpusListValidation(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,19 @@ import { SocketForm } from './SocketForm';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
onCreate(environment: Environment): void;
|
||||||
|
isDockerStandalone?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SocketTab({ onCreate }: Props) {
|
export function SocketTab({ onCreate, isDockerStandalone }: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeploymentScripts />
|
<DeploymentScripts />
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<SocketForm onCreate={onCreate} />
|
<SocketForm
|
||||||
|
onCreate={onCreate}
|
||||||
|
isDockerStandalone={isDockerStandalone}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
|
||||||
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
|
@ -6,5 +5,4 @@ export interface FormValues {
|
||||||
socketPath: string;
|
socketPath: string;
|
||||||
overridePath: boolean;
|
overridePath: boolean;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
gpus: Gpu[];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,12 +109,14 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
|
||||||
return (
|
return (
|
||||||
<APITab
|
<APITab
|
||||||
onCreate={(environment) => onCreate(environment, 'dockerApi')}
|
onCreate={(environment) => onCreate(environment, 'dockerApi')}
|
||||||
|
isDockerStandalone={isDockerStandalone}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'socket':
|
case 'socket':
|
||||||
return (
|
return (
|
||||||
<SocketTab
|
<SocketTab
|
||||||
onCreate={(environment) => onCreate(environment, 'localEndpoint')}
|
onCreate={(environment) => onCreate(environment, 'localEndpoint')}
|
||||||
|
isDockerStandalone={isDockerStandalone}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'edgeAgentStandard':
|
case 'edgeAgentStandard':
|
||||||
|
|
|
@ -11,14 +11,12 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||||
|
|
||||||
import { NameField } from '../NameField';
|
import { NameField } from '../NameField';
|
||||||
import { MoreSettingsSection } from '../MoreSettingsSection';
|
import { MoreSettingsSection } from '../MoreSettingsSection';
|
||||||
import { Hardware } from '../Hardware/Hardware';
|
|
||||||
|
|
||||||
import { EnvironmentUrlField } from './EnvironmentUrlField';
|
import { EnvironmentUrlField } from './EnvironmentUrlField';
|
||||||
import { useValidation } from './AgentForm.validation';
|
import { useValidation } from './AgentForm.validation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
onCreate(environment: Environment): void;
|
||||||
showGpus?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues: CreateAgentEnvironmentValues = {
|
const initialValues: CreateAgentEnvironmentValues = {
|
||||||
|
@ -28,10 +26,9 @@ const initialValues: CreateAgentEnvironmentValues = {
|
||||||
groupId: 1,
|
groupId: 1,
|
||||||
tagIds: [],
|
tagIds: [],
|
||||||
},
|
},
|
||||||
gpus: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AgentForm({ onCreate, showGpus = false }: Props) {
|
export function AgentForm({ onCreate }: Props) {
|
||||||
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||||
|
|
||||||
const mutation = useCreateAgentEnvironmentMutation();
|
const mutation = useCreateAgentEnvironmentMutation();
|
||||||
|
@ -50,7 +47,7 @@ export function AgentForm({ onCreate, showGpus = false }: Props) {
|
||||||
<NameField />
|
<NameField />
|
||||||
<EnvironmentUrlField />
|
<EnvironmentUrlField />
|
||||||
|
|
||||||
<MoreSettingsSection>{showGpus && <Hardware />}</MoreSettingsSection>
|
<MoreSettingsSection />
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { object, SchemaOf, string } from 'yup';
|
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 { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
import { metadataValidation } from '../MetadataFieldset/validation';
|
import { metadataValidation } from '../MetadataFieldset/validation';
|
||||||
|
@ -11,7 +10,6 @@ export function useValidation(): SchemaOf<CreateAgentEnvironmentValues> {
|
||||||
name: useNameValidation(),
|
name: useNameValidation(),
|
||||||
environmentUrl: environmentValidation(),
|
environmentUrl: environmentValidation(),
|
||||||
meta: metadataValidation(),
|
meta: metadataValidation(),
|
||||||
gpus: gpusListValidation(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { FormSection } from '@@/form-components/FormSection';
|
||||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||||
|
|
||||||
import { MoreSettingsSection } from '../../MoreSettingsSection';
|
import { MoreSettingsSection } from '../../MoreSettingsSection';
|
||||||
import { Hardware } from '../../Hardware/Hardware';
|
|
||||||
|
|
||||||
import { EdgeAgentFieldset } from './EdgeAgentFieldset';
|
import { EdgeAgentFieldset } from './EdgeAgentFieldset';
|
||||||
import { useValidationSchema } from './EdgeAgentForm.validation';
|
import { useValidationSchema } from './EdgeAgentForm.validation';
|
||||||
|
@ -26,16 +25,10 @@ import { FormValues } from './types';
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
onCreate(environment: Environment): void;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
showGpus?: boolean;
|
|
||||||
asyncMode: boolean;
|
asyncMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EdgeAgentForm({
|
export function EdgeAgentForm({ onCreate, readonly, asyncMode }: Props) {
|
||||||
onCreate,
|
|
||||||
readonly,
|
|
||||||
asyncMode,
|
|
||||||
showGpus = false,
|
|
||||||
}: Props) {
|
|
||||||
const settingsQuery = useSettings();
|
const settingsQuery = useSettings();
|
||||||
|
|
||||||
const createMutation = useCreateEdgeAgentEnvironmentMutation();
|
const createMutation = useCreateEdgeAgentEnvironmentMutation();
|
||||||
|
@ -76,7 +69,6 @@ export function EdgeAgentForm({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FormSection>
|
</FormSection>
|
||||||
{showGpus && <Hardware />}
|
|
||||||
</MoreSettingsSection>
|
</MoreSettingsSection>
|
||||||
|
|
||||||
{!readonly && (
|
{!readonly && (
|
||||||
|
@ -133,6 +125,5 @@ export function buildInitialValues(settings: Settings): FormValues {
|
||||||
PingInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
|
PingInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
|
||||||
SnapshotInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
|
SnapshotInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
|
||||||
},
|
},
|
||||||
gpus: [],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
edgeAsyncIntervalsValidation,
|
edgeAsyncIntervalsValidation,
|
||||||
EdgeAsyncIntervalsValues,
|
EdgeAsyncIntervalsValues,
|
||||||
} from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
} 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 urlValidation } from '@/react/portainer/common/PortainerTunnelAddrField';
|
||||||
import { validation as addressValidation } from '@/react/portainer/common/PortainerUrlField';
|
import { validation as addressValidation } from '@/react/portainer/common/PortainerUrlField';
|
||||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
@ -23,7 +22,6 @@ export function useValidationSchema(asyncMode: boolean): SchemaOf<FormValues> {
|
||||||
tunnelServerAddr: asyncMode ? string() : addressValidation(),
|
tunnelServerAddr: asyncMode ? string() : addressValidation(),
|
||||||
pollFrequency: number().required(),
|
pollFrequency: number().required(),
|
||||||
meta: metadataValidation(),
|
meta: metadataValidation(),
|
||||||
gpus: gpusListValidation(),
|
|
||||||
edge: isBE
|
edge: isBE
|
||||||
? edgeAsyncIntervalsValidation()
|
? edgeAsyncIntervalsValidation()
|
||||||
: (null as unknown as SchemaOf<EdgeAsyncIntervalsValues>),
|
: (null as unknown as SchemaOf<EdgeAsyncIntervalsValues>),
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
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';
|
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
|
@ -9,7 +8,6 @@ export interface FormValues {
|
||||||
tunnelServerAddr?: string;
|
tunnelServerAddr?: string;
|
||||||
pollFrequency: number;
|
pollFrequency: number;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
gpus: Gpu[];
|
|
||||||
|
|
||||||
edge: EdgeAsyncIntervalsValues;
|
edge: EdgeAsyncIntervalsValues;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ interface Props {
|
||||||
onCreate: (environment: Environment) => void;
|
onCreate: (environment: Environment) => void;
|
||||||
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
|
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
|
||||||
isNomadTokenVisible?: boolean;
|
isNomadTokenVisible?: boolean;
|
||||||
showGpus?: boolean;
|
|
||||||
asyncMode?: boolean;
|
asyncMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +22,6 @@ export function EdgeAgentTab({
|
||||||
onCreate,
|
onCreate,
|
||||||
commands,
|
commands,
|
||||||
isNomadTokenVisible,
|
isNomadTokenVisible,
|
||||||
showGpus = false,
|
|
||||||
asyncMode = false,
|
asyncMode = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
|
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
|
||||||
|
@ -35,7 +33,6 @@ export function EdgeAgentTab({
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
readonly={!!edgeInfo}
|
readonly={!!edgeInfo}
|
||||||
key={formKey}
|
key={formKey}
|
||||||
showGpus={showGpus}
|
|
||||||
asyncMode={asyncMode}
|
asyncMode={asyncMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
|
||||||
<FormSection title="Hardware acceleration">
|
|
||||||
<GpusList value={field.value} onChange={onChange} />
|
|
||||||
</FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Reference in New Issue