Compare commits
19 Commits
fix/EE-305
...
fix/EE-523
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87a33ad268 | ||
|
|
0ca56ddbb1 | ||
|
|
3a30c8ed1e | ||
|
|
151db6bfe7 | ||
|
|
106c719a34 | ||
|
|
1cfd031db1 | ||
|
|
fbc1a2d44d | ||
|
|
47478efd1e | ||
|
|
50940b7fba | ||
|
|
7468d5637b | ||
|
|
6edc210ae7 | ||
|
|
f859876cb6 | ||
|
|
5e434a82ed | ||
|
|
d9f6471a00 | ||
|
|
a7d1a20dfb | ||
|
|
17517d7521 | ||
|
|
c609f6912f | ||
|
|
346fe9e3f1 | ||
|
|
69f14e569b |
@@ -64,6 +64,7 @@
|
||||
"UseServerMetrics": false
|
||||
},
|
||||
"Flags": {
|
||||
"IsServerIngressClassDetected": false,
|
||||
"IsServerMetricsDetected": false,
|
||||
"IsServerStorageDetected": false
|
||||
},
|
||||
@@ -905,8 +906,7 @@
|
||||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": "",
|
||||
"subtleUpgradeButton": false
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
@@ -936,8 +936,7 @@
|
||||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": "",
|
||||
"subtleUpgradeButton": false
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
|
||||
@@ -82,9 +82,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
}
|
||||
|
||||
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
|
||||
@@ -342,12 +342,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27 h1:PceCpp86SDYb3lZHT4KpuBCkmcJMW5x1qrdFNEfAdUo=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a h1:4VGM1OH15fqm5rgki0eLF6vND/NxHfoPt3CA6/YdA0k=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f h1:z/lmLhZMMSIwg70Ap1rPluXNe1vQXH9gfK9K/ols4JA=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7 h1:/i985KPNw0KvVtLhTEPUa86aJMtun5ZPOyFCJzdY+dY=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -137,12 +135,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
|
||||
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to move git repository directory", err)
|
||||
}
|
||||
|
||||
repositoryUsername := ""
|
||||
repositoryPassword := ""
|
||||
if payload.RepositoryAuthentication {
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
type themePayload struct {
|
||||
// Color represents the color theme of the UI
|
||||
Color *string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
||||
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
|
||||
SubtleUpgradeButton *bool `json:"subtleUpgradeButton" example:"false"`
|
||||
}
|
||||
|
||||
type userUpdatePayload struct {
|
||||
@@ -33,11 +31,11 @@ type userUpdatePayload struct {
|
||||
|
||||
func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.Contains(payload.Username, " ") {
|
||||
return errors.New("Invalid username. Must not contain any whitespace")
|
||||
return errors.New("invalid username. Must not contain any whitespace")
|
||||
}
|
||||
|
||||
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
|
||||
return errors.New("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||
return errors.New("invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -120,10 +118,6 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||
if payload.Theme.Color != nil {
|
||||
user.ThemeSettings.Color = *payload.Theme.Color
|
||||
}
|
||||
|
||||
if payload.Theme.SubtleUpgradeButton != nil {
|
||||
user.ThemeSettings.SubtleUpgradeButton = *payload.Theme.SubtleUpgradeButton
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Role != 0 {
|
||||
|
||||
@@ -76,6 +76,16 @@ func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bo
|
||||
}
|
||||
|
||||
func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
if endpoint.Kubernetes.Flags.IsServerIngressClassDetected {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
endpoint.Kubernetes.Flags.IsServerIngressClassDetected = true
|
||||
endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
}()
|
||||
cli, err := factory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
|
||||
@@ -107,6 +117,16 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
|
||||
}
|
||||
|
||||
func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
if endpoint.Kubernetes.Flags.IsServerMetricsDetected {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||
endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
}()
|
||||
cli, err := factory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
|
||||
@@ -118,11 +138,6 @@ func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService datas
|
||||
return
|
||||
}
|
||||
endpoint.Kubernetes.Configuration.UseServerMetrics = true
|
||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||
err = endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
|
||||
return
|
||||
@@ -158,6 +173,16 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
|
||||
}
|
||||
|
||||
func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
if endpoint.Kubernetes.Flags.IsServerStorageDetected {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
|
||||
endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
}()
|
||||
log.Info().Msg("attempting to detect storage classes in the cluster")
|
||||
err := storageDetect(endpoint, endpointService, factory)
|
||||
if err == nil {
|
||||
|
||||
@@ -17,6 +17,11 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultKubeClientQPS = 30
|
||||
DefaultKubeClientBurst = 100
|
||||
)
|
||||
|
||||
type (
|
||||
// ClientFactory is used to create Kubernetes clients
|
||||
ClientFactory struct {
|
||||
@@ -113,6 +118,9 @@ func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, k
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cliConfig.QPS = DefaultKubeClientQPS
|
||||
cliConfig.Burst = DefaultKubeClientBurst
|
||||
|
||||
cli, err := kubernetes.NewForConfig(cliConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -198,7 +206,10 @@ func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernete
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Insecure = true
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &agentHeaderRoundTripper{
|
||||
@@ -217,6 +228,9 @@ func buildLocalClient() (*kubernetes.Clientset, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
return kubernetes.NewForConfig(config)
|
||||
}
|
||||
|
||||
|
||||
@@ -588,9 +588,12 @@ type (
|
||||
Flags KubernetesFlags `json:"Flags"`
|
||||
}
|
||||
|
||||
// KubernetesFlags are used to detect if we need to run initial cluster
|
||||
// detection again.
|
||||
KubernetesFlags struct {
|
||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||
IsServerIngressClassDetected bool `json:"IsServerIngressClassDetected"`
|
||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||
}
|
||||
|
||||
// KubernetesSnapshot represents a snapshot of a specific Kubernetes environment(endpoint) at a specific time
|
||||
@@ -1286,8 +1289,6 @@ type (
|
||||
UserThemeSettings struct {
|
||||
// Color represents the color theme of the UI
|
||||
Color string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
||||
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
|
||||
SubtleUpgradeButton bool `json:"subtleUpgradeButton"`
|
||||
}
|
||||
|
||||
// Webhook represents a url webhook that can be used to update a service
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
|
||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
||||
@@ -11,6 +12,8 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
|
||||
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
|
||||
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
|
||||
import { GpusInsights } from '@/react/docker/host/SetupView/GpusInsights';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.docker.react.components', [])
|
||||
@@ -45,4 +48,9 @@ export const componentsModule = angular
|
||||
'usedAllGpus',
|
||||
'enableGpuManagement',
|
||||
])
|
||||
).name;
|
||||
)
|
||||
.component(
|
||||
'gpusList',
|
||||
r2a(withControlledInput(GpusList), ['value', 'onChange'])
|
||||
)
|
||||
.component('gpusInsights', r2a(GpusInsights, [])).name;
|
||||
|
||||
@@ -151,21 +151,7 @@
|
||||
<div class="col-sm-12 form-section-title"> Other </div>
|
||||
<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>
|
||||
<gpus-insights></gpus-insights>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
|
||||
@@ -167,6 +167,6 @@ function confirmImageForceRemoval() {
|
||||
title: 'Are you sure?',
|
||||
modalType: ModalType.Destructive,
|
||||
message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.',
|
||||
confirmButton: buildConfirmButton('Remote the image', 'danger'),
|
||||
confirmButton: buildConfirmButton('Remove the image', 'danger'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInInt
|
||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.edge.react.components', [])
|
||||
.component(
|
||||
'edgeGroupsSelector',
|
||||
r2a(withReactQuery(EdgeGroupsSelector), [
|
||||
r2a(withUIRouter(withReactQuery(EdgeGroupsSelector)), [
|
||||
'onChange',
|
||||
'value',
|
||||
'error',
|
||||
|
||||
@@ -86,7 +86,6 @@ export default class CreateEdgeStackViewController {
|
||||
async $onInit() {
|
||||
try {
|
||||
this.edgeGroups = await this.EdgeGroupService.groups();
|
||||
this.noGroups = this.edgeGroups.length === 0;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
|
||||
<edge-groups-selector ng-if="!$ctrl.noGroups" value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||
<edge-groups-selector value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||
|
||||
<edge-stack-deployment-type-selector
|
||||
value="$ctrl.formValues.DeploymentType"
|
||||
|
||||
@@ -46,6 +46,8 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment && endpoint.Status === EnvironmentStatus.Down) {
|
||||
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
|
||||
}
|
||||
|
||||
await KubernetesNamespaceService.get();
|
||||
} catch (e) {
|
||||
let params = {};
|
||||
|
||||
|
||||
@@ -141,7 +141,9 @@ export default class HelmTemplatesController {
|
||||
try {
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
|
||||
const nonSystemNamespaces = resourcePools.filter((resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
const nonSystemNamespaces = resourcePools.filter(
|
||||
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||
);
|
||||
this.state.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||
this.state.resourcePool = this.state.resourcePools[0];
|
||||
} catch (err) {
|
||||
|
||||
@@ -397,14 +397,16 @@ class KubernetesApplicationHelper {
|
||||
static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) {
|
||||
const finalRes = _.map(persistedFolders, (folder) => {
|
||||
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName));
|
||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
|
||||
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
|
||||
res.Size = parseInt(pvc.Storage, 10);
|
||||
res.SizeUnit = pvc.Storage.slice(-2);
|
||||
res.ContainerPath = folder.MountPath;
|
||||
return res;
|
||||
if (pvc) {
|
||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
|
||||
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
|
||||
res.Size = parseInt(pvc.Storage, 10);
|
||||
res.SizeUnit = pvc.Storage.slice(-2);
|
||||
res.ContainerPath = folder.MountPath;
|
||||
return res;
|
||||
}
|
||||
});
|
||||
return finalRes;
|
||||
return finalRes.filter((item) => item !== undefined);
|
||||
}
|
||||
|
||||
static generateVolumesFromPersistentVolumClaims(app, volumeClaims) {
|
||||
|
||||
@@ -81,18 +81,19 @@ class KubernetesNamespaceService {
|
||||
}
|
||||
}
|
||||
|
||||
async get(name) {
|
||||
async get(name, refreshCache = false) {
|
||||
if (name) {
|
||||
return this.$async(this.getAsync, name);
|
||||
}
|
||||
const cachedAllowedNamespaces = this.LocalStorage.getAllowedNamespaces();
|
||||
if (cachedAllowedNamespaces) {
|
||||
updateNamespaces(cachedAllowedNamespaces);
|
||||
return cachedAllowedNamespaces;
|
||||
} else {
|
||||
if (!cachedAllowedNamespaces || refreshCache) {
|
||||
const allowedNamespaces = await this.getAllAsync();
|
||||
this.LocalStorage.storeAllowedNamespaces(allowedNamespaces);
|
||||
updateNamespaces(allowedNamespaces);
|
||||
return allowedNamespaces;
|
||||
} else {
|
||||
updateNamespaces(cachedAllowedNamespaces);
|
||||
return cachedAllowedNamespaces;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ export function KubernetesResourcePoolService(
|
||||
}
|
||||
|
||||
// getting the quota for all namespaces is costly by default, so disable getting it by default
|
||||
async function getAll({ getQuota = false }) {
|
||||
const namespaces = await KubernetesNamespaceService.get();
|
||||
async function getAll({ getQuota = false, refreshCache = false }) {
|
||||
const namespaces = await KubernetesNamespaceService.get('', refreshCache);
|
||||
const pools = await Promise.all(
|
||||
_.map(namespaces, async (namespace) => {
|
||||
const name = namespace.Name;
|
||||
|
||||
@@ -189,7 +189,7 @@ class KubernetesApplicationsController {
|
||||
};
|
||||
|
||||
this.state.namespaces = await this.KubernetesNamespaceService.get();
|
||||
this.state.namespaces = this.state.namespaces.filter((n) => n.Status !== 'Terminating');
|
||||
this.state.namespaces = this.state.namespaces.filter((n) => n.Status === 'Active');
|
||||
this.state.namespaces = _.sortBy(this.state.namespaces, 'Name');
|
||||
this.state.namespace = this.state.namespaces.length ? (this.state.namespaces.find((n) => n.Name === 'default') ? 'default' : this.state.namespaces[0].Name) : '';
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
</div>
|
||||
<!-- #end region IMAGE FIELD -->
|
||||
|
||||
<div class="col-sm-12 !p-0">
|
||||
<div class="col-sm-12 mb-4 !p-0">
|
||||
<annotations-be-teaser></annotations-be-teaser>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1208,22 +1208,31 @@ class KubernetesCreateApplicationController {
|
||||
]);
|
||||
this.nodesLimits = nodesLimits;
|
||||
|
||||
const nonSystemNamespaces = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
const nonSystemNamespaces = _.filter(
|
||||
resourcePools,
|
||||
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||
);
|
||||
|
||||
this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name);
|
||||
this.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||
|
||||
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
|
||||
if (!this.formValues.ResourcePool) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.state.nodes.memory and this.state.nodes.cpu are used to calculate the slider limits, so set them before calling updateSliders()
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||
this.state.nodes.cpu += item.CPU;
|
||||
});
|
||||
|
||||
if (this.resourcePools.length) {
|
||||
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
|
||||
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
|
||||
this.updateNamespaceLimits(namespaceWithQuota);
|
||||
this.updateSliders(namespaceWithQuota);
|
||||
}
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
if (!this.formValues.ResourcePool) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
|
||||
this.nodeNumber = nodes.length;
|
||||
|
||||
@@ -1281,9 +1290,6 @@ class KubernetesCreateApplicationController {
|
||||
this.formValues.IsPublishingService = this.formValues.PublishedPorts.length > 0;
|
||||
|
||||
this.oldFormValues = angular.copy(this.formValues);
|
||||
|
||||
this.updateNamespaceLimits(namespaceWithQuota);
|
||||
this.updateSliders(namespaceWithQuota);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
|
||||
@@ -196,7 +196,10 @@ class KubernetesCreateConfigurationController {
|
||||
|
||||
try {
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
this.resourcePools = _.filter(
|
||||
resourcePools,
|
||||
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||
);
|
||||
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
await this.getConfigurations();
|
||||
|
||||
@@ -165,7 +165,10 @@ class KubernetesConfigureController {
|
||||
const allResourcePools = await this.KubernetesResourcePoolService.get();
|
||||
const resourcePools = _.filter(
|
||||
allResourcePools,
|
||||
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
|
||||
(resourcePool) =>
|
||||
!KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) &&
|
||||
!KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name) &&
|
||||
resourcePool.Namespace.Status === 'Active'
|
||||
);
|
||||
|
||||
ingressesToDel.forEach((ingress) => {
|
||||
|
||||
@@ -284,7 +284,8 @@ class KubernetesDeployController {
|
||||
async getNamespacesAsync() {
|
||||
try {
|
||||
const pools = await this.KubernetesResourcePoolService.get();
|
||||
const namespaces = _.map(pools, 'Namespace').sort((a, b) => {
|
||||
let namespaces = pools.filter((pool) => pool.Namespace.Status === 'Active');
|
||||
namespaces = _.map(namespaces, 'Namespace').sort((a, b) => {
|
||||
if (a.Name === 'default') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -50,11 +50,11 @@ class KubernetesResourcePoolsController {
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
await this.KubernetesNamespaceService.refreshCacheAsync();
|
||||
this.$state.reload(this.$state.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.KubernetesNamespaceService.refreshCacheAsync();
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
@@ -77,7 +77,7 @@ class KubernetesResourcePoolsController {
|
||||
|
||||
async getResourcePoolsAsync() {
|
||||
try {
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get('', { getQuota: true });
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get('', { getQuota: true, refreshCache: true });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retreive namespaces');
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ class StackRedeployGitFormController {
|
||||
|
||||
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
|
||||
|
||||
if (this.stack.AutoUpdate.Webhook) {
|
||||
if (this.stack.AutoUpdate && this.stack.AutoUpdate.Webhook) {
|
||||
this.state.webhookId = this.stack.AutoUpdate.Webhook;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ export default class ThemeSettingsController {
|
||||
this.UserService = UserService;
|
||||
|
||||
this.setThemeColor = this.setThemeColor.bind(this);
|
||||
this.setSubtleUpgradeButton = this.setSubtleUpgradeButton.bind(this);
|
||||
}
|
||||
|
||||
async setThemeColor(color) {
|
||||
@@ -29,13 +28,6 @@ export default class ThemeSettingsController {
|
||||
});
|
||||
}
|
||||
|
||||
async setSubtleUpgradeButton(value) {
|
||||
return this.$async(async () => {
|
||||
this.state.subtleUpgradeButton = value;
|
||||
this.updateThemeSettings({ subtleUpgradeButton: value });
|
||||
});
|
||||
}
|
||||
|
||||
async updateThemeSettings(theme) {
|
||||
try {
|
||||
if (!this.state.isDemo) {
|
||||
@@ -57,7 +49,6 @@ export default class ThemeSettingsController {
|
||||
userId: null,
|
||||
themeColor: 'auto',
|
||||
isDemo: state.application.demoEnvironment.enabled,
|
||||
subtleUpgradeButton: false,
|
||||
};
|
||||
|
||||
this.state.availableThemes = options;
|
||||
@@ -67,7 +58,6 @@ export default class ThemeSettingsController {
|
||||
const user = await this.UserService.user(this.state.userId);
|
||||
|
||||
this.state.themeColor = user.ThemeSettings.color || this.state.themeColor;
|
||||
this.state.subtleUpgradeButton = !!user.ThemeSettings.subtleUpgradeButton;
|
||||
} catch (err) {
|
||||
notifyError('Failure', err, 'Unable to get user details');
|
||||
}
|
||||
|
||||
@@ -9,16 +9,6 @@
|
||||
<pr-icon icon="'alert-circle'" class-name="'icon-primary'"></pr-icon>
|
||||
<span class="small">Dark and High-contrast theme are experimental. Some UI components might not display properly.</span>
|
||||
</p>
|
||||
|
||||
<div class="mt-3">
|
||||
<por-switch-field
|
||||
tooltip="'This setting toggles a more subtle UI for the upgrade button located at the top of the sidebar'"
|
||||
label-class="'col-sm-2'"
|
||||
label="'Subtle upgrade button'"
|
||||
checked="$ctrl.state.subtleUpgradeButton"
|
||||
on-change="($ctrl.setSubtleUpgradeButton)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||
import { EdgeKeyDisplay } from '@/react/portainer/environments/ItemView/EdgeKeyDisplay';
|
||||
import { KVMControl } from '@/react/portainer/environments/KvmView/KVMControl';
|
||||
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
|
||||
|
||||
export const environmentsModule = angular
|
||||
.module('portainer.app.react.components.environments', [])
|
||||
.component('edgeKeyDisplay', r2a(EdgeKeyDisplay, ['edgeKey']))
|
||||
.component('kvmControl', r2a(KVMControl, ['deviceId', 'server', 'token']))
|
||||
.component(
|
||||
'gpusList',
|
||||
r2a(withControlledInput(GpusList), ['value', 'onChange'])
|
||||
'kvmControl',
|
||||
r2a(KVMControl, ['deviceId', 'server', 'token'])
|
||||
).name;
|
||||
|
||||
@@ -26,7 +26,6 @@ import { Slider } from '@@/form-components/Slider';
|
||||
import { TagButton } from '@@/TagButton';
|
||||
import { BETeaserButton } from '@@/BETeaserButton';
|
||||
import { CodeEditor } from '@@/CodeEditor';
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
|
||||
import { fileUploadField } from './file-upload-field';
|
||||
import { switchField } from './switch-field';
|
||||
@@ -80,10 +79,6 @@ export const componentsModule = angular
|
||||
.component('badge', r2a(Badge, ['type', 'className']))
|
||||
.component('fileUploadField', fileUploadField)
|
||||
.component('porSwitchField', switchField)
|
||||
.component(
|
||||
'insightsBox',
|
||||
r2a(InsightsBox, ['header', 'content', 'setHtmlContent', 'insightCloseId'])
|
||||
)
|
||||
.component(
|
||||
'passwordCheckHint',
|
||||
r2a(withReactQuery(PasswordCheckHint), [
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
value="$ctrl.settings.ServerType"
|
||||
options="$ctrl.boxSelectorOptions"
|
||||
on-change="($ctrl.onChangeServerType)"
|
||||
slim="true"
|
||||
></box-selector>
|
||||
|
||||
<ldap-settings-custom
|
||||
|
||||
@@ -22,6 +22,5 @@ export type User = {
|
||||
};
|
||||
ThemeSettings: {
|
||||
color: 'dark' | 'light' | 'highcontrast' | 'auto';
|
||||
subtleUpgradeButton: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -211,22 +211,7 @@
|
||||
<!-- !open-amt info -->
|
||||
<!-- gpus info -->
|
||||
<div class="mb-4">
|
||||
<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>
|
||||
<gpus-insights></gpus-insights>
|
||||
</div>
|
||||
<!-- gpus info -->
|
||||
<div class="form-group">
|
||||
|
||||
@@ -19,7 +19,6 @@ export function createMockUsers(
|
||||
PortainerAuthorizations: {},
|
||||
ThemeSettings: {
|
||||
color: 'auto',
|
||||
subtleUpgradeButton: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import clsx from 'clsx';
|
||||
import { Icon as ReactFeatherComponentType, Check } from 'lucide-react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
import { getFeatureDetails } from '@@/BEFeatureIndicator/utils';
|
||||
|
||||
import styles from './BoxSelectorItem.module.css';
|
||||
import { BoxSelectorOption, Value } from './types';
|
||||
@@ -36,7 +36,9 @@ export function BoxSelectorItem<T extends Value>({
|
||||
slim = false,
|
||||
checkIcon = Check,
|
||||
}: Props<T>) {
|
||||
const limitedToBE = isLimitedToBE(option.feature);
|
||||
const { limitedToBE = false, url: featureUrl } = getFeatureDetails(
|
||||
option.feature
|
||||
);
|
||||
|
||||
const beIndicatorTooltipId = `box-selector-item-${radioName}-${option.id}-limited`;
|
||||
|
||||
@@ -57,7 +59,12 @@ export function BoxSelectorItem<T extends Value>({
|
||||
type={type}
|
||||
checkIcon={checkIcon}
|
||||
>
|
||||
{limitedToBE && <LimitedToBeIndicator tooltipId={beIndicatorTooltipId} />}
|
||||
{limitedToBE && (
|
||||
<LimitedToBeIndicator
|
||||
tooltipId={beIndicatorTooltipId}
|
||||
url={featureUrl}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={clsx('flex gap-2', {
|
||||
'opacity-30': limitedToBE,
|
||||
@@ -89,15 +96,15 @@ export function BoxSelectorItem<T extends Value>({
|
||||
return <BadgeIcon icon={option.icon} />;
|
||||
}
|
||||
|
||||
if (option.iconType === 'logo') {
|
||||
return <LogoIcon icon={option.icon} />;
|
||||
if (option.iconType === 'raw') {
|
||||
return (
|
||||
<Icon
|
||||
icon={option.icon}
|
||||
className={clsx(styles.icon, '!flex items-center')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon
|
||||
icon={option.icon}
|
||||
className={clsx(styles.icon, '!flex items-center')}
|
||||
/>
|
||||
);
|
||||
return <LogoIcon icon={option.icon} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||
import { getFeatureDetails } from '@@/BEFeatureIndicator/utils';
|
||||
|
||||
interface Props {
|
||||
tooltipId: string;
|
||||
featureId?: FeatureId;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export function LimitedToBeIndicator({ featureId, tooltipId }: Props) {
|
||||
const { url } = getFeatureDetails(featureId);
|
||||
|
||||
export function LimitedToBeIndicator({ tooltipId, url }: Props) {
|
||||
return (
|
||||
<div className="absolute left-0 top-0 w-full">
|
||||
<div className="mx-auto flex max-w-fit items-center gap-1 rounded-b-lg bg-warning-4 py-1 px-3 text-sm">
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<span className="text-warning-9">BE Feature</span>
|
||||
<a
|
||||
className="text-warning-9"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
BE Feature
|
||||
</a>
|
||||
<TooltipWithChildren
|
||||
position="bottom"
|
||||
|
||||
@@ -9,6 +9,7 @@ export const kubernetes: BoxSelectorOption<'kubernetes'> = {
|
||||
label: 'Kubernetes',
|
||||
description: 'Kubernetes manifest format',
|
||||
value: 'kubernetes',
|
||||
iconType: 'logo',
|
||||
};
|
||||
|
||||
export const compose: BoxSelectorOption<'compose'> = {
|
||||
@@ -17,4 +18,5 @@ export const compose: BoxSelectorOption<'compose'> = {
|
||||
label: 'Compose',
|
||||
description: 'docker-compose format',
|
||||
value: 'compose',
|
||||
iconType: 'logo',
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import { Lightbulb, X } from 'lucide-react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import sanitize from 'sanitize-html';
|
||||
import { ReactNode } from 'react';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
@@ -15,25 +14,11 @@ export type Props = {
|
||||
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) {
|
||||
export function InsightsBox({ header, content, 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;
|
||||
}
|
||||
@@ -44,10 +29,16 @@ export function InsightsBox({
|
||||
<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')}>
|
||||
<p
|
||||
className={clsx(
|
||||
// text-[0.9em] matches .form-horizontal .control-label font-size used in many labels in portainer
|
||||
'mb-2 text-[0.9em] font-medium',
|
||||
insightCloseId && 'pr-4'
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
</p>
|
||||
<div>{htmlContent || content}</div>
|
||||
<div className="small">{content}</div>
|
||||
</div>
|
||||
{insightCloseId && (
|
||||
<Button
|
||||
|
||||
@@ -26,7 +26,6 @@ export function createMockUser(id: number, username: string): UserViewModel {
|
||||
AuthenticationMethod: '',
|
||||
ThemeSettings: {
|
||||
color: 'auto',
|
||||
subtleUpgradeButton: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +63,11 @@ export function Button<TasProps = unknown>({
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
className={clsx(`btn btn-${color}`, sizeClass(size), className)}
|
||||
onClick={onClick}
|
||||
onClick={(e) => {
|
||||
if (!disabled) {
|
||||
onClick?.(e);
|
||||
}
|
||||
}}
|
||||
title={title}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...ariaProps}
|
||||
|
||||
29
app/react/docker/host/SetupView/GpusInsights.tsx
Normal file
29
app/react/docker/host/SetupView/GpusInsights.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
|
||||
export function GpusInsights() {
|
||||
return (
|
||||
<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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -64,11 +64,7 @@ function InnerSelector({
|
||||
}) {
|
||||
const edgeGroupsQuery = useEdgeGroups();
|
||||
|
||||
if (!edgeGroupsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = edgeGroupsQuery.data.filter(isGroupVisible);
|
||||
const items = (edgeGroupsQuery.data || []).filter(isGroupVisible);
|
||||
|
||||
const valueGroups = _.compact(
|
||||
value.map((id) => items.find((item) => item.Id === id))
|
||||
|
||||
@@ -40,6 +40,7 @@ export function EdgeStackDeploymentTypeSelector({
|
||||
hasDockerEndpoint
|
||||
? 'Cannot use this option with Edge Docker environments'
|
||||
: '',
|
||||
iconType: 'logo',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
19
app/react/hooks/useStateWrapper.ts
Normal file
19
app/react/hooks/useStateWrapper.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
export function useStateWrapper<T>(value: T, onChange: (value: T) => void) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
const updateInputValue = useCallback(
|
||||
(value: T) => {
|
||||
setInputValue(value);
|
||||
onChange(value);
|
||||
},
|
||||
[onChange, setInputValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
return [inputValue, updateInputValue] as const;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export function AnnotationsBeTeaser() {
|
||||
message={
|
||||
<div className="vertical-center">
|
||||
<span>
|
||||
You can specify{' '}
|
||||
Allows specifying of{' '}
|
||||
<a
|
||||
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
|
||||
target="_black"
|
||||
@@ -32,7 +32,6 @@ export function AnnotationsBeTeaser() {
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
setHtmlMessage
|
||||
/>
|
||||
</div>
|
||||
<div className="block">
|
||||
|
||||
@@ -206,7 +206,7 @@ export function IngressForm({
|
||||
message={
|
||||
<div className="vertical-center">
|
||||
<span>
|
||||
You can specify{' '}
|
||||
Allows specifying of{' '}
|
||||
<a
|
||||
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
|
||||
target="_black"
|
||||
@@ -224,7 +224,6 @@ export function IngressForm({
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
setHtmlMessage
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,8 +96,10 @@ export function KubeconfigPrompt({
|
||||
<Checkbox
|
||||
id={`${env.Id}`}
|
||||
label={`${env.Name} (${env.URL})`}
|
||||
checked={!!selection[env.Id]}
|
||||
onChange={() => toggleSelection(env.Id, !selection[env.Id])}
|
||||
checked={selection.includes(env.Id)}
|
||||
onChange={() =>
|
||||
toggleSelection(env.Id, !selection.includes(env.Id))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const existingEnvironmentTypes: EnvironmentOption[] = [
|
||||
label: 'Kubernetes',
|
||||
icon: Kubernetes,
|
||||
iconType: 'logo',
|
||||
description: 'Connect to a kubernetes environment via URL/IP',
|
||||
description: 'Connect to a Kubernetes environment via URL/IP',
|
||||
},
|
||||
{
|
||||
id: 'aci',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { useStateWrapper } from '@/react/hooks/useStateWrapper';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { InputList, ItemProps } from '@@/form-components/InputList';
|
||||
import { useCaretPosition } from '@@/form-components/useCaretPosition';
|
||||
|
||||
interface Props {
|
||||
value: Array<string>;
|
||||
@@ -32,21 +33,19 @@ function Item({
|
||||
error,
|
||||
readOnly,
|
||||
}: ItemProps<string>) {
|
||||
const { ref, updateCaret } = useCaretPosition();
|
||||
const [inputValue, updateInputValue] = useStateWrapper(item, onChange);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col">
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon>path</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
mRef={ref}
|
||||
required
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
value={item}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
updateCaret();
|
||||
updateInputValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useStateWrapper } from '@/react/hooks/useStateWrapper';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { useCaretPosition } from '@@/form-components/useCaretPosition';
|
||||
|
||||
import { GitFormModel } from '../types';
|
||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||
@@ -25,7 +26,7 @@ export function ComposePathField({
|
||||
isDockerStandalone,
|
||||
errors,
|
||||
}: Props) {
|
||||
const { ref, updateCaret } = useCaretPosition();
|
||||
const [inputValue, updateInputValue] = useStateWrapper(value, onChange);
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
@@ -65,11 +66,9 @@ export function ComposePathField({
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
mRef={ref}
|
||||
value={value}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
updateCaret();
|
||||
updateInputValue(e.target.value);
|
||||
}}
|
||||
placeholder={isCompose ? 'docker-compose.yml' : 'manifest.yml'}
|
||||
/>
|
||||
|
||||
@@ -12,8 +12,6 @@ import clsx from 'clsx';
|
||||
import { useSearch } from '@/react/portainer/gitops/queries/useSearch';
|
||||
import { useDebounce } from '@/react/hooks/useDebounce';
|
||||
|
||||
import { useCaretPosition } from '@@/form-components/useCaretPosition';
|
||||
|
||||
import { getAuthentication } from '../utils';
|
||||
import { GitFormModel } from '../types';
|
||||
|
||||
@@ -30,7 +28,7 @@ export function PathSelector({
|
||||
placeholder: string;
|
||||
model: GitFormModel;
|
||||
}) {
|
||||
const [searchTerm, setSearchTerm] = useDebounce('', () => {});
|
||||
const [searchTerm, setSearchTerm] = useDebounce(value, onChange);
|
||||
|
||||
const creds = getAuthentication(model);
|
||||
const payload = {
|
||||
@@ -43,7 +41,6 @@ export function PathSelector({
|
||||
model.RepositoryURL && model.RepositoryURLValid && searchTerm
|
||||
);
|
||||
const { data: searchResults } = useSearch(payload, enabled);
|
||||
const { ref, updateCaret } = useCaretPosition();
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@@ -53,11 +50,10 @@ export function PathSelector({
|
||||
data-cy="component-gitComposeInput"
|
||||
>
|
||||
<ComboboxInput
|
||||
ref={ref}
|
||||
className="form-control"
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{searchResults && searchResults.length > 0 && (
|
||||
<ComboboxPopover>
|
||||
@@ -81,12 +77,9 @@ export function PathSelector({
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
setSearchTerm(e.target.value);
|
||||
onChange(e.target.value);
|
||||
updateCaret();
|
||||
}
|
||||
|
||||
function onSelect(value: string) {
|
||||
setSearchTerm('');
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PropsWithChildren, ReactNode } from 'react';
|
||||
import { SchemaOf, string } from 'yup';
|
||||
|
||||
import { StackId } from '@/react/docker/stacks/types';
|
||||
import { useStateWrapper } from '@/react/hooks/useStateWrapper';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
@@ -29,6 +30,8 @@ export function RefField({
|
||||
isUrlValid,
|
||||
stackId,
|
||||
}: Props) {
|
||||
const [inputValue, updateInputValue] = useStateWrapper(value, onChange);
|
||||
|
||||
return isBE ? (
|
||||
<Wrapper
|
||||
errors={error}
|
||||
@@ -62,8 +65,8 @@ export function RefField({
|
||||
}
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={inputValue}
|
||||
onChange={(e) => updateInputValue(e.target.value)}
|
||||
placeholder="refs/heads/main"
|
||||
/>
|
||||
</Wrapper>
|
||||
|
||||
@@ -5,12 +5,11 @@ import Microsoft from '@/assets/ico/vendor/microsoft.svg?c';
|
||||
import Ldap from '@/assets/ico/ldap.svg?c';
|
||||
import OAuth from '@/assets/ico/oauth.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'auth_internal',
|
||||
icon: <BadgeIcon icon={ArrowDownCircle} />,
|
||||
icon: ArrowDownCircle,
|
||||
iconType: 'badge',
|
||||
label: 'Internal',
|
||||
description: 'Internal authentication mechanism',
|
||||
value: 1,
|
||||
@@ -20,6 +19,7 @@ export const options = [
|
||||
icon: Ldap,
|
||||
label: 'LDAP',
|
||||
description: 'LDAP authentication',
|
||||
iconType: 'logo',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
@@ -27,6 +27,7 @@ export const options = [
|
||||
icon: Microsoft,
|
||||
label: 'Microsoft Active Directory',
|
||||
description: 'AD authentication',
|
||||
iconType: 'logo',
|
||||
value: 4,
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
@@ -35,6 +36,7 @@ export const options = [
|
||||
icon: OAuth,
|
||||
label: 'OAuth',
|
||||
description: 'OAuth authentication',
|
||||
iconType: 'logo',
|
||||
value: 3,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Edit } from 'lucide-react';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import Openldap from '@/assets/ico/vendor/openldap.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
const SERVER_TYPES = {
|
||||
CUSTOM: 0,
|
||||
OPEN_LDAP: 1,
|
||||
@@ -14,7 +12,8 @@ const SERVER_TYPES = {
|
||||
export const options = [
|
||||
{
|
||||
id: 'ldap_custom',
|
||||
icon: <BadgeIcon icon={Edit} />,
|
||||
icon: Edit,
|
||||
iconType: 'badge',
|
||||
label: 'Custom',
|
||||
value: SERVER_TYPES.CUSTOM,
|
||||
},
|
||||
@@ -24,5 +23,6 @@ export const options = [
|
||||
label: 'OpenLDAP',
|
||||
value: SERVER_TYPES.OPEN_LDAP,
|
||||
feature: FeatureId.EXTERNAL_AUTH_LDAP,
|
||||
iconType: 'logo',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,8 +5,6 @@ import Microsoft from '@/assets/ico/vendor/microsoft.svg?c';
|
||||
import Google from '@/assets/ico/vendor/google.svg?c';
|
||||
import Github from '@/assets/ico/vendor/github.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'microsoft',
|
||||
@@ -14,6 +12,7 @@ export const options = [
|
||||
label: 'Microsoft',
|
||||
description: 'Microsoft OAuth provider',
|
||||
value: 'microsoft',
|
||||
iconType: 'logo',
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
{
|
||||
@@ -22,6 +21,7 @@ export const options = [
|
||||
label: 'Google',
|
||||
description: 'Google OAuth provider',
|
||||
value: 'google',
|
||||
iconType: 'logo',
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
{
|
||||
@@ -30,11 +30,13 @@ export const options = [
|
||||
label: 'Github',
|
||||
description: 'Github OAuth provider',
|
||||
value: 'github',
|
||||
iconType: 'logo',
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
icon: <BadgeIcon icon={Edit} />,
|
||||
icon: Edit,
|
||||
iconType: 'badge',
|
||||
label: 'Custom',
|
||||
description: 'Custom OAuth provider',
|
||||
value: 'custom',
|
||||
|
||||
@@ -22,7 +22,6 @@ export function mockExampleData() {
|
||||
Role: 2,
|
||||
ThemeSettings: {
|
||||
color: 'auto',
|
||||
subtleUpgradeButton: false,
|
||||
},
|
||||
EndpointAuthorizations: {},
|
||||
PortainerAuthorizations: {
|
||||
@@ -50,7 +49,6 @@ export function mockExampleData() {
|
||||
Role: 2,
|
||||
ThemeSettings: {
|
||||
color: 'auto',
|
||||
subtleUpgradeButton: false,
|
||||
},
|
||||
EndpointAuthorizations: {},
|
||||
PortainerAuthorizations: {
|
||||
|
||||
@@ -64,19 +64,13 @@ function UpgradeBEBanner() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subtleButton = userQuery.data.ThemeSettings.subtleUpgradeButton;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'flex w-full items-center justify-center gap-1 py-2 pr-2 hover:underline',
|
||||
{
|
||||
'border-0 bg-warning-5 font-semibold text-warning-9': !subtleButton,
|
||||
'border border-solid border-blue-9 bg-[#023959] font-medium text-white th-dark:border-[#343434] th-dark:bg-black':
|
||||
subtleButton,
|
||||
},
|
||||
'border border-solid border-blue-9 bg-[#023959] font-medium text-white th-dark:border-[#343434] th-dark:bg-black',
|
||||
'th-highcontrast:border th-highcontrast:border-solid th-highcontrast:border-white th-highcontrast:bg-black th-highcontrast:font-medium th-highcontrast:text-white'
|
||||
)}
|
||||
onClick={handleClick}
|
||||
@@ -84,11 +78,7 @@ function UpgradeBEBanner() {
|
||||
<ArrowUpCircle
|
||||
className={clsx(
|
||||
'lucide text-lg',
|
||||
{
|
||||
'fill-warning-9 stroke-warning-5': !subtleButton,
|
||||
'fill-warning-6 stroke-[#023959] th-dark:stroke-black':
|
||||
subtleButton,
|
||||
},
|
||||
'fill-warning-6 stroke-[#023959] th-dark:stroke-black',
|
||||
'th-highcontrast:fill-warning-6 th-highcontrast:stroke-black'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,6 @@ const mockUser: User = {
|
||||
Username: 'mock',
|
||||
ThemeSettings: {
|
||||
color: 'auto',
|
||||
subtleUpgradeButton: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user