Compare commits

...

19 Commits

Author SHA1 Message Date
Prabhat Khera
87a33ad268 fix error when editiing non-exitent pvc 2023-03-20 16:16:32 +13:00
Oscar Zhou
0ca56ddbb1 fix(stack/git): fix cursor movement issue in git text fields (#8656) 2023-03-20 10:00:35 +13:00
Chaim Lev-Ari
3a30c8ed1e fix(ui/box-selector): BE link and use icons standard size [EE-5133] (#8659) 2023-03-19 13:37:44 +01:00
Ali
151db6bfe7 fix(kubeconfig): fix download checkbox [EE-5199] (#8675)
Co-authored-by: testa113 <testa113>
2023-03-17 10:34:00 +13:00
Ali
106c719a34 fix(wizard): Capitalise Kubernetes [EE-5178] (#8663)
Co-authored-by: testa113 <testa113>
2023-03-16 18:50:58 +13:00
Dakota Walsh
1cfd031db1 fix(kubernetes): Prevent rerunning initial cluster detection [EE-5170] (#8667) 2023-03-16 15:39:43 +13:00
Prabhat Khera
fbc1a2d44d fix(ui): namespace cache refresh on reload EE-5155 (#8657) 2023-03-16 10:10:26 +13:00
Oscar Zhou
47478efd1e fix(stack/git): remove duplicate code used to backup compose dir (#8620) 2023-03-15 12:27:23 +13:00
Ali
50940b7fba fix(annotations) ingress tip to match ee [EE-5158] (#8654)
Co-authored-by: testa113 <testa113>
2023-03-14 10:41:41 +13:00
matias-portainer
7468d5637b fix(upgrade): remove yellow upgrade banner EE-5141 (#8641) 2023-03-13 09:01:39 -03:00
Ali
6edc210ae7 fix(kube): check for ns on enter [EE-5160] (#8648)
Co-authored-by: testa113 <testa113>
2023-03-13 13:57:07 +13:00
Prabhat Khera
f859876cb6 fix typo in delete image modal dialog (#8622) 2023-03-13 11:05:55 +13:00
Matt Hook
5e434a82ed reduce throttling in the kube client (#8631) 2023-03-13 09:47:23 +13:00
Ali
d9f6471a00 fix(annotation): update wording/styling [EE-5158] (#8643)
Co-authored-by: testa113 <testa113>
2023-03-10 16:52:15 +13:00
cmeng
a7d1a20dfb fix(edge-stack) always show edge group selector [EE-5157] (#8638) 2023-03-10 10:48:53 +13:00
Ali
17517d7521 fix(app): restrict ns fix create app [EE-5123] (#8633)
Co-authored-by: testa113 <testa113>
2023-03-10 10:24:20 +13:00
andres-portainer
c609f6912f fix(home): disable live connect for async [EE-5000] (#8628) 2023-03-09 15:50:36 -03:00
Ali
346fe9e3f1 refactor(GPU): colocate and update UI [EE-5127] (#8634)
Co-authored-by: testa113 <testa113>
2023-03-09 22:06:49 +13:00
matias-portainer
69f14e569b fix(stacks): pass WorkingDir to deployer command EE-5142 (#8624) 2023-03-08 19:34:50 -03:00
59 changed files with 261 additions and 237 deletions

View File

@@ -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": "",

View File

@@ -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")
}

View File

@@ -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=

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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'),
});
}

View File

@@ -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',

View File

@@ -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');
}

View File

@@ -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"

View File

@@ -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 = {};

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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) : '';

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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) => {

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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), [

View File

@@ -14,6 +14,7 @@
value="$ctrl.settings.ServerType"
options="$ctrl.boxSelectorOptions"
on-change="($ctrl.onChangeServerType)"
slim="true"
></box-selector>
<ldap-settings-custom

View File

@@ -22,6 +22,5 @@ export type User = {
};
ThemeSettings: {
color: 'dark' | 'light' | 'highcontrast' | 'auto';
subtleUpgradeButton: boolean;
};
};

View File

@@ -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">

View File

@@ -19,7 +19,6 @@ export function createMockUsers(
PortainerAuthorizations: {},
ThemeSettings: {
color: 'auto',
subtleUpgradeButton: false,
},
}));
}

View File

@@ -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} />;
}
}

View File

@@ -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"

View File

@@ -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',
};

View File

@@ -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

View File

@@ -26,7 +26,6 @@ export function createMockUser(id: number, username: string): UserViewModel {
AuthenticationMethod: '',
ThemeSettings: {
color: 'auto',
subtleUpgradeButton: false,
},
};
}

View File

@@ -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}

View 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 -&gt; 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"
/>
);
}

View File

@@ -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))

View File

@@ -40,6 +40,7 @@ export function EdgeStackDeploymentTypeSelector({
hasDockerEndpoint
? 'Cannot use this option with Edge Docker environments'
: '',
iconType: 'logo',
},
];

View 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;
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
))}

View File

@@ -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',

View File

@@ -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>

View File

@@ -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'}
/>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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,
},
];

View File

@@ -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',
},
];

View File

@@ -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',

View File

@@ -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: {

View File

@@ -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'
)}
/>

View File

@@ -10,7 +10,6 @@ const mockUser: User = {
Username: 'mock',
ThemeSettings: {
color: 'auto',
subtleUpgradeButton: false,
},
};