Compare commits

..

23 Commits

Author SHA1 Message Date
Steven Kang
27cbc3818e version: bump version to 2.27.0-rc1 (#363)
Co-authored-by: steven <steven@stevens-Mini.hub>
2025-02-03 12:36:29 +13:00
James Player
e943aa8f03 feat(documentation): change docs to use LTS/STS instead of version number (#357) 2025-02-03 11:17:36 +13:00
James Player
17a4750d8e fix(kubernetes): Resource reservation wasn't displaying properly in business edition and remove leader status (#362) 2025-02-03 11:02:23 +13:00
Malcolm Lockyer
7d18c22aa1 fix(ui): bring back k8s applications page row expand published urls [r8s-145] (#356) 2025-01-31 13:16:18 +13:00
Ali
c80cc6e268 chore(automation): give unique selectors [r8s-168] (#345)
Co-authored-by: JamesPlayer <james.player@portainer.io>
2025-01-30 15:42:32 +13:00
andres-portainer
b30a1b5250 fix(edgestacks): avoid repeated statuses BE-11561 (#351) 2025-01-27 16:00:05 -03:00
LP B
b753371700 fix(app/edge-stack): edge stack create form validation (#343) 2025-01-24 17:02:52 +01:00
andres-portainer
3ca5ab180f fix(system): optimize the memory usage when counting nodes BE-11575 (#342) 2025-01-23 20:41:09 -03:00
Ali
4971f5510c fix(app): edit app with configmap [r8s-95] (#341) 2025-01-24 11:35:47 +13:00
andres-portainer
20fa7e508d fix(edgestacks): decouple the EdgeStackStatusUpdateCoordinator so it can be used by other packages BE-11572 (#340) 2025-01-23 17:10:46 -03:00
James Player
ebffc340d9 fix(k8s): Changed 'Deploy from file' button text to 'Deploy from code' (#338) 2025-01-23 16:47:52 +13:00
andres-portainer
9a86737caa fix(edgestacks): add a status update coordinator to increase performance BE-11572 (#337) 2025-01-22 20:24:54 -03:00
Steven Kang
d35d8a7307 feat(oauth): fix mapping (#330) 2025-01-23 09:03:51 +13:00
andres-portainer
701ff5d6bc refactor(edgestacks): move handlerDBErr() out of the handler BE-11572 (#336) 2025-01-22 16:35:06 -03:00
LP B
9044b25a23 fix(app): remove passwords from registries list response (#334) 2025-01-22 17:40:21 +01:00
Ali
7f089fab86 fix(apps): use replicas from application spec [r8s-142] (#335) 2025-01-22 12:31:27 +13:00
James Carppe
a259c28678 Update bug report template for 2.26.1 (#329) 2025-01-21 16:19:03 +13:00
LP B
db48da185a fix(app/editor): reduce editor slowness by debouncing onChange calls (#326) 2025-01-17 22:41:06 +01:00
LP B
cab667c23b fix(app/edge-stack): UI notification on creation error (#325) 2025-01-17 20:33:01 +01:00
andres-portainer
154ca9f1b1 fix(edge): return proper error from context BE-11564 (#323) 2025-01-16 20:18:51 -03:00
Oscar Zhou
2abe40b786 fix(edgestack): remove project folder after deleting edgestack [BE-11559] (#320) 2025-01-16 09:16:09 +13:00
James Carppe
6be2420b32 Update bug report template for 2.26.0 (#319) 2025-01-15 14:38:59 +13:00
Ali
9405cc0e04 chore(portainer): bump version to 2.26.0 (#302) 2025-01-14 07:20:11 +13:00
70 changed files with 628 additions and 303 deletions

View File

@@ -95,6 +95,8 @@ body:
description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.26.1'
- '2.26.0'
- '2.25.1'
- '2.25.0'
- '2.24.1'

View File

@@ -610,7 +610,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.26.0",
"KubectlShellImage": "portainer/kubectl-shell:2.27.0-rc1",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -943,7 +943,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.26.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.27.0-rc1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}

View File

@@ -3,6 +3,7 @@ package edgestacks
import (
"errors"
"net/http"
"strconv"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
@@ -52,10 +53,14 @@ func (handler *Handler) deleteEdgeStack(tx dataservices.DataStoreTx, edgeStackID
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", err)
}
err = handler.edgeStacksService.DeleteEdgeStack(tx, edgeStack.ID, edgeStack.EdgeGroups)
if err != nil {
if err := handler.edgeStacksService.DeleteEdgeStack(tx, edgeStack.ID, edgeStack.EdgeGroups); err != nil {
return httperror.InternalServerError("Unable to delete edge stack", err)
}
stackFolder := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(edgeStack.ID)))
if err := handler.FileService.RemoveDirectory(stackFolder); err != nil {
return httperror.InternalServerError("Unable to remove edge stack project folder", err)
}
return nil
}

View File

@@ -1,12 +1,14 @@
package edgestacks
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
"github.com/segmentio/encoding/json"
)
@@ -101,3 +103,52 @@ func TestDeleteInvalidEdgeStack(t *testing.T) {
})
}
}
func TestDeleteEdgeStack_RemoveProjectFolder(t *testing.T) {
handler, rawAPIKey := setupHandler(t)
edgeGroup := createEdgeGroup(t, handler.DataStore)
payload := edgeStackFromStringPayload{
Name: "test-stack",
DeploymentType: portainer.EdgeStackDeploymentCompose,
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
StackFileContent: "version: '3.7'\nservices:\n test:\n image: test",
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
t.Fatal("error encoding payload:", err)
}
// Create
req, err := http.NewRequest(http.MethodPost, "/edge_stacks/create/string", &buf)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected a %d response, found: %d", http.StatusNoContent, rec.Code)
}
assert.DirExists(t, handler.FileService.GetEdgeStackProjectPath("1"))
// Delete
if req, err = http.NewRequest(http.MethodDelete, "/edge_stacks/1", nil); err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("expected a %d response, found: %d", http.StatusNoContent, rec.Code)
}
assert.NoDirExists(t, handler.FileService.GetEdgeStackProjectPath("1"))
}

View File

@@ -34,7 +34,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if err != nil {
return handler.handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
return handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
}
fileName := stack.EntryPoint

View File

@@ -30,7 +30,7 @@ func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request)
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
if err != nil {
return handler.handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
return handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
}
return response.JSON(w, edgeStack)

View File

@@ -63,7 +63,7 @@ func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Req
func (handler *Handler) deleteEdgeStackStatus(tx dataservices.DataStoreTx, stackID portainer.EdgeStackID, endpoint *portainer.Endpoint) (*portainer.EdgeStack, error) {
stack, err := tx.EdgeStack().EdgeStack(stackID)
if err != nil {
return nil, handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
return nil, handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
}
environmentStatus, ok := stack.Status[endpoint.ID]

View File

@@ -4,11 +4,11 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -69,15 +69,21 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return httperror.BadRequest("Invalid request payload", fmt.Errorf("edge polling error: %w. Environment ID: %d", err, payload.EndpointID))
}
var stack *portainer.EdgeStack
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if r.Context().Err() != nil {
return err
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(payload.EndpointID)
if err != nil {
return handlerDBErr(fmt.Errorf("unable to find the environment from the database: %w. Environment ID: %d", err, payload.EndpointID), "unable to find the environment")
}
stack, err = handler.updateEdgeStackStatus(tx, r, portainer.EdgeStackID(stackID), payload)
return err
}); err != nil {
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
}
updateFn := func(stack *portainer.EdgeStack) (*portainer.EdgeStack, error) {
return handler.updateEdgeStackStatus(stack, endpoint, r, stack.ID, payload)
}
stack, err := handler.stackCoordinator.UpdateStatus(r, portainer.EdgeStackID(stackID), updateFn)
if err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr
@@ -93,36 +99,11 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return response.JSON(w, stack)
}
func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *http.Request, stackID portainer.EdgeStackID, payload updateStatusPayload) (*portainer.EdgeStack, error) {
stack, err := tx.EdgeStack().EdgeStack(stackID)
if err != nil {
if dataservices.IsErrObjectNotFound(err) {
// Skip error because agent tries to report on deleted stack
log.Debug().
Err(err).
Int("stackID", int(stackID)).
Int("status", int(*payload.Status)).
Msg("Unable to find a stack inside the database, skipping error")
return nil, nil
}
return nil, fmt.Errorf("unable to retrieve Edge stack from the database: %w. Environment ID: %d", err, payload.EndpointID)
}
func (handler *Handler) updateEdgeStackStatus(stack *portainer.EdgeStack, endpoint *portainer.Endpoint, r *http.Request, stackID portainer.EdgeStackID, payload updateStatusPayload) (*portainer.EdgeStack, error) {
if payload.Version > 0 && payload.Version < stack.Version {
return stack, nil
}
endpoint, err := tx.Endpoint().Endpoint(payload.EndpointID)
if err != nil {
return nil, handler.handlerDBErr(fmt.Errorf("unable to find the environment from the database: %w. Environment ID: %d", err, payload.EndpointID), "unable to find the environment")
}
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
return nil, httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
}
status := *payload.Status
log.Debug().
@@ -138,10 +119,6 @@ func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *ht
updateEnvStatus(payload.EndpointID, stack, deploymentStatus)
if err := tx.EdgeStack().UpdateEdgeStack(stackID, stack); err != nil {
return nil, handler.handlerDBErr(fmt.Errorf("unable to update Edge stack to the database: %w. Environment name: %s", err, endpoint.Name), "unable to update Edge stack")
}
return stack, nil
}
@@ -160,7 +137,11 @@ func updateEnvStatus(environmentId portainer.EndpointID, stack *portainer.EdgeSt
}
}
environmentStatus.Status = append(environmentStatus.Status, deploymentStatus)
if containsStatus := slices.ContainsFunc(environmentStatus.Status, func(e portainer.EdgeStackDeploymentStatus) bool {
return e.Type == deploymentStatus.Type
}); !containsStatus {
environmentStatus.Status = append(environmentStatus.Status, deploymentStatus)
}
stack.Status[environmentId] = environmentStatus
}

View File

@@ -0,0 +1,150 @@
package edgestacks
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
type statusRequest struct {
respCh chan statusResponse
stackID portainer.EdgeStackID
updateFn statusUpdateFn
}
type statusResponse struct {
Stack *portainer.EdgeStack
Error error
}
type statusUpdateFn func(*portainer.EdgeStack) (*portainer.EdgeStack, error)
type EdgeStackStatusUpdateCoordinator struct {
updateCh chan statusRequest
dataStore dataservices.DataStore
}
var errAnotherStackUpdateInProgress = errors.New("another stack update is in progress")
func NewEdgeStackStatusUpdateCoordinator(dataStore dataservices.DataStore) *EdgeStackStatusUpdateCoordinator {
return &EdgeStackStatusUpdateCoordinator{
updateCh: make(chan statusRequest),
dataStore: dataStore,
}
}
func (c *EdgeStackStatusUpdateCoordinator) Start() {
for {
c.loop()
}
}
func (c *EdgeStackStatusUpdateCoordinator) loop() {
u := <-c.updateCh
respChs := []chan statusResponse{u.respCh}
var stack *portainer.EdgeStack
err := c.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
// 1. Load the edge stack
var err error
stack, err = loadEdgeStack(tx, u.stackID)
if err != nil {
return err
}
// 2. Mutate the edge stack opportunistically until there are no more pending updates
for {
stack, err = u.updateFn(stack)
if err != nil {
return err
}
if m, ok := c.getNextUpdate(stack.ID); ok {
u = m
} else {
break
}
respChs = append(respChs, u.respCh)
}
// 3. Save the changes back to the database
if err := tx.EdgeStack().UpdateEdgeStack(stack.ID, stack); err != nil {
return handlerDBErr(fmt.Errorf("unable to update Edge stack: %w.", err), "Unable to persist the stack changes inside the database")
}
return nil
})
// 4. Send back the responses
for _, ch := range respChs {
ch <- statusResponse{Stack: stack, Error: err}
}
}
func loadEdgeStack(tx dataservices.DataStoreTx, stackID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
stack, err := tx.EdgeStack().EdgeStack(stackID)
if err != nil {
if dataservices.IsErrObjectNotFound(err) {
// Skip the error when the agent tries to update the status on a deleted stack
log.Debug().
Err(err).
Int("stackID", int(stackID)).
Msg("Unable to find a stack inside the database, skipping error")
return nil, nil
}
return nil, fmt.Errorf("unable to retrieve Edge stack from the database: %w.", err)
}
return stack, nil
}
func (c *EdgeStackStatusUpdateCoordinator) getNextUpdate(stackID portainer.EdgeStackID) (statusRequest, bool) {
for {
select {
case u := <-c.updateCh:
// Discard the update and let the agent retry
if u.stackID != stackID {
u.respCh <- statusResponse{Error: errAnotherStackUpdateInProgress}
continue
}
return u, true
default:
return statusRequest{}, false
}
}
}
func (c *EdgeStackStatusUpdateCoordinator) UpdateStatus(r *http.Request, stackID portainer.EdgeStackID, updateFn statusUpdateFn) (*portainer.EdgeStack, error) {
respCh := make(chan statusResponse)
defer close(respCh)
msg := statusRequest{
respCh: respCh,
stackID: stackID,
updateFn: updateFn,
}
select {
case c.updateCh <- msg:
r := <-respCh
return r.Stack, r.Error
case <-r.Context().Done():
return nil, r.Context().Err()
}
}

View File

@@ -51,10 +51,14 @@ func setupHandler(t *testing.T) (*Handler, string) {
t.Fatal(err)
}
coord := NewEdgeStackStatusUpdateCoordinator(store)
go coord.Start()
handler := NewHandler(
security.NewRequestBouncer(store, jwtService, apiKeyService),
store,
edgestacks.NewService(store),
coord,
)
handler.FileService = fs
@@ -144,3 +148,15 @@ func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID port
return edgeStack
}
func createEdgeGroup(t *testing.T, store dataservices.DataStore) portainer.EdgeGroup {
edgeGroup := portainer.EdgeGroup{
ID: 1,
Name: "EdgeGroup 1",
}
if err := store.EdgeGroup().Create(&edgeGroup); err != nil {
t.Fatal(err)
}
return edgeGroup
}

View File

@@ -80,7 +80,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID portainer.EdgeStackID, payload updateEdgeStackPayload) (*portainer.EdgeStack, error) {
stack, err := tx.EdgeStack().EdgeStack(stackID)
if err != nil {
return nil, handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
return nil, handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
}
relationConfig, err := edge.FetchEndpointRelationsConfig(tx)

View File

@@ -22,15 +22,17 @@ type Handler struct {
GitService portainer.GitService
edgeStacksService *edgestackservice.Service
KubernetesDeployer portainer.KubernetesDeployer
stackCoordinator *EdgeStackStatusUpdateCoordinator
}
// NewHandler creates a handler to manage environment(endpoint) group operations.
func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStore, edgeStacksService *edgestackservice.Service) *Handler {
func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStore, edgeStacksService *edgestackservice.Service, stackCoordinator *EdgeStackStatusUpdateCoordinator) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
DataStore: dataStore,
edgeStacksService: edgeStacksService,
stackCoordinator: stackCoordinator,
}
h.Handle("/edge_stacks/create/{method}",
@@ -58,10 +60,10 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
return h
}
func (handler *Handler) handlerDBErr(err error, msg string) *httperror.HandlerError {
func handlerDBErr(err error, msg string) *httperror.HandlerError {
httpErr := httperror.InternalServerError(msg, err)
if handler.DataStore.IsErrObjectNotFound(err) {
if dataservices.IsErrObjectNotFound(err) {
httpErr.StatusCode = http.StatusNotFound
}

View File

@@ -36,5 +36,9 @@ func (handler *Handler) registryList(w http.ResponseWriter, r *http.Request) *ht
return httperror.InternalServerError("Unable to retrieve registries from the database", err)
}
for idx := range registries {
hideFields(&registries[idx], false)
}
return response.JSON(w, registries)
}

View File

@@ -3,6 +3,7 @@ package system
import (
"net/http"
portainer "github.com/portainer/portainer/api"
statusutil "github.com/portainer/portainer/api/internal/nodes"
"github.com/portainer/portainer/api/internal/snapshot"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
@@ -31,14 +32,15 @@ func (handler *Handler) systemNodesCount(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Failed to get environment list", err)
}
for i := range endpoints {
err = snapshot.FillSnapshotData(handler.dataStore, &endpoints[i])
if err != nil {
var nodes int
for _, endpoint := range endpoints {
if err := snapshot.FillSnapshotData(handler.dataStore, &endpoint); err != nil {
return httperror.InternalServerError("Unable to add snapshot data", err)
}
}
nodes := statusutil.NodesCount(endpoints)
nodes += statusutil.NodesCount([]portainer.Endpoint{endpoint})
}
return response.JSON(w, &nodesCountResponse{Nodes: nodes})
}

View File

@@ -161,7 +161,10 @@ func (server *Server) Start() error {
edgeJobsHandler.FileService = server.FileService
edgeJobsHandler.ReverseTunnelService = server.ReverseTunnelService
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer, server.DataStore, server.EdgeStacksService)
edgeStackCoordinator := edgestacks.NewEdgeStackStatusUpdateCoordinator(server.DataStore)
go edgeStackCoordinator.Start()
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer, server.DataStore, server.EdgeStacksService, edgeStackCoordinator)
edgeStacksHandler.FileService = server.FileService
edgeStacksHandler.GitService = server.GitService
edgeStacksHandler.KubernetesDeployer = server.KubernetesDeployer

View File

@@ -50,7 +50,7 @@ func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings
return "", err
}
maps.Copy(idToken, resource)
maps.Copy(resource, idToken)
username, err := GetUsername(resource, configuration.UserIdentifier)
if err != nil {

View File

@@ -1636,9 +1636,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.26.0"
APIVersion = "2.27.0-rc1"
// Support annotation for the API version ("STS" for Short-Term Support or "LTS" for Long-Term Support)
APIVersionSupport = "STS"
APIVersionSupport = "LTS"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -5,6 +5,7 @@ class KubernetesConfigurationConverter {
static secretToConfiguration(secret) {
const res = new KubernetesConfiguration();
res.Kind = KubernetesConfigurationKinds.SECRET;
res.kind = 'Secret';
res.Id = secret.Id;
res.Name = secret.Name;
res.Type = secret.Type;
@@ -19,8 +20,15 @@ class KubernetesConfigurationConverter {
res.IsRegistrySecret = secret.IsRegistrySecret;
res.SecretType = secret.SecretType;
if (secret.Annotations) {
const serviceAccountAnnotation = secret.Annotations.find((a) => a.key === 'kubernetes.io/service-account.name');
res.ServiceAccountName = serviceAccountAnnotation ? serviceAccountAnnotation.value : undefined;
const serviceAccountKey = 'kubernetes.io/service-account.name';
if (typeof secret.Annotations === 'object') {
res.ServiceAccountName = secret.Annotations[serviceAccountKey];
} else if (Array.isArray(secret.Annotations)) {
const serviceAccountAnnotation = secret.Annotations.find((a) => a.key === 'kubernetes.io/service-account.name');
res.ServiceAccountName = serviceAccountAnnotation ? serviceAccountAnnotation.value : undefined;
} else {
res.ServiceAccountName = undefined;
}
}
res.Labels = secret.Labels;
return res;
@@ -29,6 +37,7 @@ class KubernetesConfigurationConverter {
static configMapToConfiguration(configMap) {
const res = new KubernetesConfiguration();
res.Kind = KubernetesConfigurationKinds.CONFIGMAP;
res.kind = 'ConfigMap';
res.Id = configMap.Id;
res.Name = configMap.Name;
res.Namespace = configMap.Namespace;

View File

@@ -9,6 +9,7 @@ const _KubernetesConfigurationFormValues = Object.freeze({
Name: '',
ConfigurationOwner: '',
Kind: KubernetesConfigurationKinds.CONFIGMAP,
kind: 'ConfigMap',
Data: [],
DataYaml: '',
IsSimple: true,

View File

@@ -22,29 +22,6 @@
</kubernetes-resource-reservation>
</form>
<!-- !resource-reservation -->
<!-- leader-status -->
<div ng-if="ctrl.systemEndpoints.length > 0">
<div class="col-sm-12 form-section-title"> Leader status </div>
<table class="table">
<tbody>
<tr class="text-muted">
<td style="border-top: none; width: 25%">Component</td>
<td style="border-top: none; width: 25%">Leader node</td>
</tr>
<tr ng-repeat="ep in ctrl.systemEndpoints">
<td style="width: 25%">
{{ ep.Name }}
</td>
<td style="width: 25%">
{{ ep.HolderIdentity }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- !leader-status -->
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -1,4 +1,4 @@
<page-header ng-if="ctrl.state.viewReady" title="'Create from file'" breadcrumbs="['Deploy Kubernetes resources']" reload="true"></page-header>
<page-header ng-if="ctrl.state.viewReady" title="'Create from code'" breadcrumbs="['Deploy Kubernetes resources']" reload="true"></page-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>

View File

@@ -10,8 +10,13 @@ import {
import { notifyError } from '@/portainer/services/notifications';
/**
* @deprecated use withGlobalError
* `onError` and other callbacks are not supported on react-query v5
* @deprecated for `useQuery` ONLY. Use `withGlobalError`.
*
* `onError` and other callbacks are not supported on `useQuery` in react-query v5
*
* Using `withError` is fine for mutations (`useMutation`)
*
* see https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose
*/
export function withError(fallbackMessage?: string, title = 'Failure') {
return {

View File

@@ -0,0 +1,52 @@
/* eslint-disable no-console */
import { intersection } from 'lodash';
import { useEffect, useRef } from 'react';
function logPropDifferences(
newProps: Record<string, unknown>,
lastProps: Record<string, unknown>,
verbose: boolean
) {
const allKeys = intersection(Object.keys(newProps), Object.keys(lastProps));
const changedKeys: string[] = [];
allKeys.forEach((key) => {
const newValue = newProps[key];
const lastValue = lastProps[key];
if (newValue !== lastValue) {
changedKeys.push(key);
}
});
if (changedKeys.length) {
if (verbose) {
changedKeys.forEach((key) => {
const newValue = newProps[key];
const lastValue = lastProps[key];
console.log('Key [', key, '] changed');
console.log('From: ', lastValue);
console.log('To: ', newValue);
console.log('------');
});
} else {
console.log('Changed keys: ', changedKeys.join());
}
}
}
export function useDebugPropChanges(
newProps: Record<string, unknown>,
verbose: boolean = true
) {
const lastProps = useRef<Record<string, unknown>>();
// Should only run when the component re-mounts
useEffect(() => {
console.log('Mounted');
}, []);
if (lastProps.current) {
logPropDifferences(newProps, lastProps.current, verbose);
}
lastProps.current = newProps;
}
/* eslint-enable no-console */

View File

@@ -3,7 +3,7 @@ import { StreamLanguage, LanguageSupport } from '@codemirror/language';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { createTheme } from '@uiw/codemirror-themes';
import { tags as highlightTags } from '@lezer/highlight';
@@ -11,6 +11,8 @@ import { AutomationTestingProps } from '@/types';
import { CopyButton } from '@@/buttons/CopyButton';
import { useDebounce } from '../hooks/useDebounce';
import styles from './CodeEditor.module.css';
import { TextTip } from './Tip/TextTip';
import { StackVersionSelector } from './StackVersionSelector';
@@ -89,17 +91,17 @@ export function CodeEditor({
return extensions;
}, [type]);
function handleVersionChange(version: number) {
if (versions && versions.length > 1) {
if (version < versions[0]) {
setIsRollback(true);
} else {
setIsRollback(false);
const handleVersionChange = useCallback(
(version: number) => {
if (versions && versions.length > 1) {
setIsRollback(version < versions[0]);
}
}
onVersionChange?.(version);
},
[onVersionChange, versions]
);
onVersionChange?.(version);
}
const [debouncedValue, debouncedOnChange] = useDebounce(value, onChange);
return (
<>
@@ -136,8 +138,8 @@ export function CodeEditor({
<CodeMirror
className={styles.root}
theme={theme}
value={value}
onChange={onChange}
value={debouncedValue}
onChange={debouncedOnChange}
readOnly={readonly || isRollback}
id={id}
extensions={extensions}

View File

@@ -40,17 +40,10 @@ export function useDocsUrl(doc?: string): string {
}
let url = 'https://docs.portainer.io/';
if (versionQuery.data) {
let { ServerVersion } = versionQuery.data;
if (ServerVersion[0] === 'v') {
ServerVersion = ServerVersion.substring(1);
}
const parts = ServerVersion.split('.');
if (parts.length >= 2) {
const version = parts.slice(0, 2).join('.');
url += `v/${version}`;
}
// Add LTS or STS version if we have it
if (versionQuery.data?.VersionSupport) {
url += versionQuery.data.VersionSupport.toLowerCase();
}
if (doc) {

View File

@@ -104,6 +104,7 @@ export function TagSelector({
onCreateOption={handleCreateOption}
aria-label="Tags"
data-cy="environment-tags-selector"
id="environment-tags-selector"
/>
</FormControl>
</>

View File

@@ -36,6 +36,7 @@ export function UsersSelector({
onChange(selectedUsers.map((user) => user.Id))
}
data-cy={dataCy}
id={dataCy}
inputId={inputId}
placeholder={placeholder}
isDisabled={disabled}

View File

@@ -18,6 +18,7 @@ export function Select<T extends number | string>({
options,
className,
'data-cy': dataCy,
id,
...props
}: Props<T> & SelectHTMLAttributes<HTMLSelectElement>) {
return (

View File

@@ -111,6 +111,7 @@ export function SingleSelect<TValue = string>({
onChange={(option) => onChange(option ? option.value : null)}
isOptionDisabled={(option) => !!option.disabled}
data-cy={dataCy}
id={dataCy}
inputId={inputId}
placeholder={placeholder}
isDisabled={disabled}
@@ -177,6 +178,7 @@ export function MultiSelect<TValue = string>({
closeMenuOnSelect={false}
onChange={(newValue) => onChange(newValue.map((option) => option.value))}
data-cy={dataCy}
id={dataCy}
inputId={inputId}
placeholder={placeholder}
isDisabled={disabled}

View File

@@ -65,6 +65,7 @@ export function Select<
}: Props<Option, IsMulti, Group> &
AutomationTestingProps & {
isItemVisible?: (item: Option, search: string) => boolean;
id: string;
}) {
const Component = isCreatable ? ReactSelectCreatable : ReactSelect;
const { options } = props;

View File

@@ -152,6 +152,7 @@ export function GpuFieldset({
options={options}
components={{ MultiValueRemove }}
data-cy="docker-containers-gpu-select"
id="docker-containers-gpu-select"
/>
</div>
)}
@@ -173,6 +174,7 @@ export function GpuFieldset({
components={{ Option }}
onChange={onChangeSelectedCaps}
data-cy="docker-containers-gpu-capabilities-select"
id="docker-containers-gpu-capabilities-select"
/>
</div>
</div>

View File

@@ -44,6 +44,7 @@ export function VolumeSelector({
onChange={(vol) => onChange(vol?.Name)}
inputId={inputId}
data-cy="docker-containers-volume-selector"
id="docker-containers-volume-selector"
size="sm"
/>
);

View File

@@ -43,6 +43,7 @@ export function CreatableSelector({
isDisabled={isLoading}
closeMenuOnSelect={false}
data-cy="edge-devices-assignment-selector"
id="edge-devices-assignment-selector"
/>
);

View File

@@ -45,6 +45,7 @@ export function GroupSelector() {
placeholder="Select a group"
isClearable
data-cy="edge-devices-assignment-selector"
id="edge-devices-assignment-selector"
/>
);

View File

@@ -29,13 +29,16 @@ export function CreateForm() {
const [webhookId] = useState(() => createWebhookId());
const [templateParams, setTemplateParams] = useTemplateParams();
const templateQuery = useTemplate(templateParams.type, templateParams.id);
const templateQuery = useTemplate(
templateParams.templateType,
templateParams.templateId
);
const validation = useValidation(templateQuery);
const mutation = useCreate({
webhookId,
template: templateQuery.customTemplate || templateQuery.appTemplate,
templateType: templateParams.type,
templateType: templateParams.templateType,
});
const initialValues = useInitialValues(templateQuery, templateParams);
@@ -53,6 +56,7 @@ export function CreateForm() {
initialValues={initialValues}
onSubmit={mutation.onSubmit}
validationSchema={validation}
validateOnMount
>
<InnerForm
webhookId={webhookId}
@@ -118,8 +122,8 @@ function useInitialValues(
customTemplate: CustomTemplate | undefined;
},
templateParams: {
id: number | undefined;
type: 'app' | 'custom' | undefined;
templateId: number | undefined;
templateType: 'app' | 'custom' | undefined;
}
) {
const template = templateQuery.customTemplate || templateQuery.appTemplate;
@@ -139,7 +143,7 @@ function useInitialValues(
staggerConfig:
templateQuery.customTemplate?.EdgeSettings?.StaggerConfig ??
getDefaultStaggerConfig(),
method: templateParams.id ? 'template' : 'editor',
method: templateParams.templateId ? 'template' : 'editor',
git: toGitFormModel(
templateQuery.customTemplate?.GitConfig,
parseAutoUpdateResponse()
@@ -149,19 +153,19 @@ function useInitialValues(
getDefaultRelativePathModel(),
enableWebhook: false,
fileContent: '',
templateValues: getTemplateValues(templateParams.type, template),
templateValues: getTemplateValues(templateParams.templateType, template),
useManifestNamespaces: false,
}),
[
templateQuery.customTemplate,
templateParams.id,
templateParams.type,
templateParams.templateId,
templateParams.templateType,
template,
]
);
if (
templateParams.id &&
templateParams.templateId &&
!templateQuery.customTemplate &&
!templateQuery.appTemplate
) {

View File

@@ -17,7 +17,11 @@ import { useCurrentUser } from '@/react/hooks/useUser';
import { relativePathValidation } from '@/react/portainer/gitops/RelativePathFieldset/validation';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
import { DeployMethod, GitFormModel } from '@/react/portainer/gitops/types';
import {
DeployMethod,
GitFormModel,
RelativePathModel,
} from '@/react/portainer/gitops/types';
import { EnvironmentType } from '@/react/portainer/environments/types';
import { envVarValidation } from '@@/form-components/EnvironmentVariablesFieldset';
@@ -133,7 +137,10 @@ export function useValidation({
);
},
}) as SchemaOf<GitFormModel>,
relativePath: relativePathValidation(),
relativePath: mixed().when('method', {
is: 'repository',
then: () => relativePathValidation(),
}) as SchemaOf<RelativePathModel>,
useManifestNamespaces: boolean().default(false),
})
),

View File

@@ -1,5 +1,5 @@
import { FormikErrors, useFormikContext } from 'formik';
import { SetStateAction } from 'react';
import { SetStateAction, useCallback } from 'react';
import { GitForm } from '@/react/portainer/gitops/GitForm';
import { baseEdgeStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
@@ -28,8 +28,8 @@ const buildMethods = [editor, upload, git, edgeStackTemplate] as const;
interface Props {
webhookId: string;
onChangeTemplate: (change: {
type: 'app' | 'custom' | undefined;
id: number | undefined;
templateType: 'app' | 'custom' | undefined;
templateId: number | undefined;
}) => void;
}
@@ -37,6 +37,23 @@ export function DockerComposeForm({ webhookId, onChangeTemplate }: Props) {
const { errors, values, setValues } = useFormikContext<DockerFormValues>();
const { method } = values;
const handleChange = useCallback(
(newValues: Partial<DockerFormValues>) => {
setValues((values) => ({
...values,
...newValues,
}));
},
[setValues]
);
const saveFileContent = useCallback(
(value: string) => {
handleChange({ fileContent: value });
},
[handleChange]
);
return (
<>
<FormSection title="Build Method">
@@ -59,8 +76,8 @@ export function DockerComposeForm({ webhookId, onChangeTemplate }: Props) {
values.templateValues
);
onChangeTemplate({
id: templateValues.templateId,
type: templateValues.type,
templateId: templateValues.templateId,
templateType: templateValues.type,
});
setValues((values) => ({
...values,
@@ -91,7 +108,7 @@ export function DockerComposeForm({ webhookId, onChangeTemplate }: Props) {
{method === editor.value && (
<DockerContentField
value={values.fileContent}
onChange={(value) => handleChange({ fileContent: value })}
onChange={saveFileContent}
error={errors?.fileContent}
/>
)}
@@ -128,6 +145,7 @@ export function DockerComposeForm({ webhookId, onChangeTemplate }: Props) {
<FormSection title="Advanced configurations">
<RelativePathFieldset
values={values.relativePath}
errors={errors.relativePath}
gitModel={values.git}
onChange={(relativePath) =>
setValues((values) => ({
@@ -145,13 +163,6 @@ export function DockerComposeForm({ webhookId, onChangeTemplate }: Props) {
)}
</>
);
function handleChange(newValues: Partial<DockerFormValues>) {
setValues((values) => ({
...values,
...newValues,
}));
}
}
type TemplateContentFieldProps = {

View File

@@ -29,11 +29,11 @@ export function InnerForm({
webhookId: string;
isLoading: boolean;
onChangeTemplate: ({
type,
id,
templateType,
templateId,
}: {
type: 'app' | 'custom' | undefined;
id: number | undefined;
templateType: 'app' | 'custom' | undefined;
templateId: number | undefined;
}) => void;
}) {
const { values, setFieldValue, errors, setValues, setFieldError, isValid } =
@@ -128,6 +128,7 @@ export function InnerForm({
<StaggerFieldset
isEdit={false}
values={values.staggerConfig}
errors={errors.staggerConfig}
onChange={(newStaggerValues) =>
setFieldValue('staggerConfig', newStaggerValues)
}

View File

@@ -50,6 +50,7 @@ export function TemplateSelector({
onChange(getTemplate({ type, id: templateId }), type);
}}
data-cy="edge-stacks-create-template-selector"
id="edge-stacks-create-template-selector"
/>
{isLoadingValues && (
<InlineLoader>Loading template values...</InlineLoader>

View File

@@ -6,6 +6,7 @@ import { TemplateViewModel } from '@/react/portainer/templates/app-templates/vie
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
import { notifySuccess } from '@/portainer/services/notifications';
import { transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
import { mutationOptions, withError } from '@/react-tools/react-query';
import {
BasePayload,
@@ -49,12 +50,18 @@ export function useCreate({
),
});
mutation.mutate(getPayload(method, values), {
onSuccess: () => {
notifySuccess('Success', 'Edge stack created');
router.stateService.go('^');
},
});
mutation.mutate(
getPayload(method, values),
mutationOptions(
{
onSuccess: () => {
notifySuccess('Success', 'Edge stack created');
router.stateService.go('^');
},
},
withError('unable to create edge stack')
)
);
function getPayload(
method: 'string' | 'file' | 'git',

View File

@@ -1,15 +1,14 @@
import { useParamsState } from '@/react/hooks/useParamState';
export function useTemplateParams() {
const [{ id, type }, setTemplateParams] = useParamsState(
['templateId', 'templateType'],
const [{ templateId, templateType }, setTemplateParams] = useParamsState(
(params) => ({
id: parseTemplateId(params.templateId),
type: parseTemplateType(params.templateType),
templateId: parseTemplateId(params.templateId),
templateType: parseTemplateType(params.templateType),
})
);
return [{ id, type }, setTemplateParams] as const;
return [{ templateId, templateType }, setTemplateParams] as const;
}
function parseTemplateId(param?: string) {

View File

@@ -97,6 +97,7 @@ function InnerSelector({
placeholder="Select one or multiple group(s)"
closeMenuOnSelect={false}
data-cy="edge-stacks-groups-selector"
id="edge-stacks-groups-selector"
inputId={inputId}
/>
) : (

View File

@@ -102,6 +102,7 @@ export function PrivateRegistryFieldset({
onChange={(value) => onSelect(value?.Id)}
className="w-full"
data-cy="private-registry-selector"
id="private-registry-selector"
/>
{method !== 'repository' && (
<Button

View File

@@ -22,32 +22,15 @@ export function useParamState<T>(
/** Use this when you need to use/update multiple params at once. */
export function useParamsState<T extends Record<string, unknown>>(
params: string[],
parseParams: (params: Record<string, string | undefined>) => T
) {
const { params: stateParams } = useCurrentStateAndParams();
const router = useRouter();
const state = parseParams(
params.reduce(
(acc, param) => {
acc[param] = stateParams[param];
return acc;
},
{} as Record<string, string | undefined>
)
);
const state = parseParams(stateParams);
function setState(newState: Partial<T>) {
const newStateParams = Object.entries(newState).reduce(
(acc, [key, value]) => {
acc[key] = value;
return acc;
},
{} as Record<string, unknown>
);
router.stateService.go('.', newStateParams, { reload: false });
router.stateService.go('.', newState, { reload: false });
}
return [state, setState] as const;

View File

@@ -122,6 +122,7 @@ export function AppIngressPathForm({
onChangeIngressPath(newIngressPath);
}}
data-cy="k8sAppCreate-ingressPathHostSelect"
id="k8sAppCreate-ingressPathHostSelect"
/>
<InputGroup.ButtonWrapper>
<Button

View File

@@ -7,7 +7,7 @@ export function HelmInsightsBox() {
content={
<span>
From 2.20 and on, the Helm menu sidebar option has moved to the{' '}
<strong>Create from file screen</strong> - accessed via the button
<strong>Create from code screen</strong> - accessed via the button
above.
</span>
}

View File

@@ -7,64 +7,74 @@ import { Icon } from '@@/Icon';
import { Application } from './types';
export function PublishedPorts({ item }: { item: Application }) {
const urls = getPublishedUrls(item);
const urlsWithTypes = getPublishedUrls(item);
if (urls.length === 0) {
if (urlsWithTypes.length === 0) {
return null;
}
return (
<div className="published-url-container">
<div>
<div className="text-muted"> Published URL(s) </div>
</div>
<div>
{urls.map((url) => (
<div key={url}>
<a
href={url}
target="_blank"
className="publish-url-link vertical-center"
rel="noreferrer"
>
<Icon icon={ExternalLinkIcon} />
{url}
</a>
</div>
<div className="published-url-container pl-10 flex">
<div className="text-muted mr-12">Published URL(s)</div>
<div className="flex flex-col">
{urlsWithTypes.map(({ url, type }) => (
<a
key={url}
href={url}
target="_blank"
className="publish-url-link vertical-center mb-1"
rel="noreferrer"
>
<Icon icon={ExternalLinkIcon} />
{type && (
<span className="text-muted w-24 inline-block">{type}</span>
)}
<span>{url}</span>
</a>
))}
</div>
</div>
);
}
function getClusterIPUrls(services?: Application['Services']) {
return (
services?.flatMap(
(service) =>
(service.spec?.type === 'ClusterIP' &&
service.spec?.ports?.map((port) => ({
url: `${getSchemeFromPort(port.port)}://${service?.spec
?.clusterIP}:${port.port}`,
type: 'ClusterIP',
}))) ||
[]
) || []
);
}
function getNodePortUrls(services?: Application['Services']) {
return (
services?.flatMap(
(service) =>
(service.spec?.type === 'NodePort' &&
service.spec?.ports?.map((port) => ({
url: `${getSchemeFromPort(port.port)}://${
window.location.hostname
}:${port.nodePort}`,
type: 'NodePort',
}))) ||
[]
) || []
);
}
export function getPublishedUrls(item: Application) {
// Map all ingress rules in published ports to their respective URLs
const ingressUrls =
item.PublishedPorts?.flatMap((pp) => pp.IngressRules)
.filter(({ Host, IP }) => Host || IP)
.map(({ Host, IP, Path, TLS }) => {
const scheme =
TLS &&
TLS.filter((tls) => tls.hosts && tls.hosts.includes(Host)).length > 0
? 'https'
: 'http';
return `${scheme}://${Host || IP}${Path}`;
}) || [];
// Get URLs from clusterIP and nodePort services
const clusterIPs = getClusterIPUrls(item.Services);
const nodePortUrls = getNodePortUrls(item.Services);
// Map all load balancer service ports to ip address
const loadBalancerURLs =
(item.LoadBalancerIPAddress &&
item.PublishedPorts?.map(
(pp) =>
`${getSchemeFromPort(pp.Port)}://${item.LoadBalancerIPAddress}:${
pp.Port
}`
)) ||
[];
// combine all urls
const publishedUrls = [...clusterIPs, ...nodePortUrls];
// combine ingress urls
const publishedUrls = [...ingressUrls, ...loadBalancerURLs];
// Return the first URL - priority given to ingress urls, then services (load balancers)
return publishedUrls.length > 0 ? publishedUrls : [];
}

View File

@@ -60,6 +60,7 @@ export function ConfigurationItem({
onChange={onSelectConfigMap}
size="sm"
data-cy={`k8sAppCreate-add${configurationType}Select_${index}`}
id={`k8sAppCreate-add${configurationType}Select_${index}`}
/>
</InputGroup>
{formikError?.selectedConfiguration && (

View File

@@ -144,6 +144,7 @@ export function PersistedFolderItem({
applicationValues.Containers.length > 1
}
data-cy={`k8sAppCreate-persistentFolderSizeUnitSelect_${index}`}
id={`k8sAppCreate-persistentFolderSizeUnitSelect_${index}`}
/>
</InputGroup>
{formikError?.size && <FormError>{formikError?.size}</FormError>}
@@ -175,6 +176,7 @@ export function PersistedFolderItem({
storageClasses.length <= 1
}
data-cy={`k8sAppCreate-storageSelect_${index}`}
id={`k8sAppCreate-storageSelect_${index}`}
/>
</InputGroup>
</>
@@ -207,6 +209,7 @@ export function PersistedFolderItem({
availableVolumes.length < 1
}
data-cy={`k8sAppCreate-pvcSelect_${index}`}
id={`k8sAppCreate-pvcSelect_${index}`}
/>
</InputGroup>
)}

View File

@@ -49,6 +49,7 @@ export function PlacementItem({
className={clsx({ striked: !!item.needsDeletion })}
isDisabled={!!item.needsDeletion}
data-cy={`k8sAppCreate-placementLabel_${index}`}
id={`k8sAppCreate-placementLabel_${index}`}
/>
{placementError?.label && (
<FormError>{placementError.label}</FormError>
@@ -65,6 +66,7 @@ export function PlacementItem({
className={clsx({ striked: !!item.needsDeletion })}
isDisabled={!!item.needsDeletion}
data-cy={`k8sAppCreate-placementName_${index}`}
id={`k8sAppCreate-placementName_${index}`}
/>
{placementError?.value && (
<FormError>{placementError.value}</FormError>

View File

@@ -69,11 +69,10 @@ export function getTotalPods(
): number {
switch (application.kind) {
case 'Deployment':
return application.status?.replicas ?? 0;
case 'StatefulSet':
return application.spec?.replicas ?? 0;
case 'DaemonSet':
return application.status?.desiredNumberScheduled ?? 0;
case 'StatefulSet':
return application.status?.replicas ?? 0;
default:
throw new Error('Unknown application type');
}

View File

@@ -35,6 +35,7 @@ export function StorageAccessModeSelector({
inputId={inputId}
placeholder="Not configured"
data-cy={`kubeSetup-storageAccessSelect${storageClassName}`}
id={`kubeSetup-storageAccessSelect${storageClassName}`}
/>
);
}

View File

@@ -43,6 +43,7 @@ export function NamespacesSelector({
onChange(selectedTeams.map((namespace) => namespace.name))
}
data-cy={dataCy}
id={dataCy}
inputId={inputId}
placeholder={placeholder}
/>

View File

@@ -18,7 +18,7 @@ export function CreateFromManifestButton({
}}
data-cy={dataCy}
>
Create from file
Create from code
</AddButton>
);
}

View File

@@ -184,6 +184,7 @@ export function IngressForm({
}
noOptionsMessage={() => 'No namespaces available'}
data-cy="k8sAppCreate-namespaceSelect"
id="k8sAppCreate-namespaceSelect"
/>
)}
</div>
@@ -266,6 +267,7 @@ export function IngressForm({
}
noOptionsMessage={() => 'No ingress classes available'}
data-cy="k8sAppCreate-ingressClassSelect"
id="k8sAppCreate-ingressClassSelect"
/>
{errors.className && (
<FormError className="error-inline mt-1">
@@ -464,6 +466,7 @@ export function IngressForm({
noOptionsMessage={() => 'No TLS secrets available'}
size="sm"
data-cy={`k8sAppCreate-tlsSelect_${hostIndex}`}
id={`k8sAppCreate-tlsSelect_${hostIndex}`}
/>
{!host.NoHost && (
<div className="input-group-btn">

View File

@@ -35,6 +35,7 @@ export function NamespaceAccessUsersSelector({
closeMenuOnSelect={false}
onChange={onChange}
data-cy={dataCy}
id={dataCy}
inputId={inputId}
placeholder={placeholder}
components={{ MultiValueLabel, Option: OptionComponent }}

View File

@@ -67,6 +67,7 @@ export function RegistriesSelector({
onChange={onChange}
inputId={inputId}
data-cy="namespaceCreate-registrySelect"
id="namespaceCreate-registrySelect"
placeholder="Select one or more registries"
isDisabled={isEditingDisabled}
/>

View File

@@ -64,7 +64,9 @@ export function VolumesDatatable() {
settingsManager={tableState}
title="Volumes"
titleIcon={Database}
getRowId={(row) => row.PersistentVolumeClaim.Name}
getRowId={(row) =>
`${row.PersistentVolumeClaim.Name}-${row.ResourcePool.Namespace.Name}`
}
disableSelect={!hasWriteAuth}
isRowSelectable={({ original: volume }) =>
!isSystemNamespace(volume.ResourcePool.Namespace.Name, namespaces) &&

View File

@@ -37,6 +37,7 @@ export function PorAccessManagementUsersSelector({
closeMenuOnSelect={false}
onChange={onChange}
data-cy="component-selectUser"
id="component-selectUser"
inputId="users-selector"
placeholder="Select one or more users and/or teams"
components={{ MultiValueLabel, Option: OptionComponent }}

View File

@@ -30,7 +30,7 @@ export function TeamsField({
'You can select which team(s) will be able to manage this resource.'
: undefined
}
inputId="teams-selector"
inputId="authorized-teams-selector"
errors={errors}
>
{teams.length > 0 ? (
@@ -39,7 +39,7 @@ export function TeamsField({
teams={teams}
onChange={onChange}
value={value}
inputId="teams-selector"
inputId="authorized-teams-selector"
dataCy="teams-selector"
/>
) : (

View File

@@ -21,7 +21,7 @@ export function UsersField({ name, users, value, onChange, errors }: Props) {
? 'You can select which user(s) will be able to manage this resource.'
: undefined
}
inputId="users-selector"
inputId="authorized-users-selector"
errors={errors}
>
{users.length > 0 ? (
@@ -30,7 +30,7 @@ export function UsersField({ name, users, value, onChange, errors }: Props) {
users={users}
onChange={onChange}
value={value}
inputId="users-selector"
inputId="authorized-users-selector"
dataCy="users-selector"
/>
) : (

View File

@@ -26,6 +26,7 @@ export function PorAccessControlFormTeamSelector({
closeMenuOnSelect={false}
onChange={onChange}
data-cy="portainer-selectTeamAccess"
id="portainer-selectTeamAccess"
inputId={inputId}
placeholder="Select one or more teams"
/>

View File

@@ -26,6 +26,7 @@ export function PorAccessControlFormUserSelector({
closeMenuOnSelect={false}
onChange={onChange}
data-cy="portainer-selectUserAccess"
id="portainer-selectUserAccess"
inputId={inputId}
placeholder="Select one or more users"
/>

View File

@@ -94,7 +94,7 @@ function HelmDatatableDescription({ isAdmin }: { isAdmin: boolean }) {
) : (
<span>globally-set Helm repo</span>
)}
) and shown in the Create from file screen&apos;s Helm charts list.
) and shown in the Create from code screen&apos;s Helm charts list.
</TextTip>
);
}

View File

@@ -112,6 +112,7 @@ export function TimeWindowPickerInputGroup({
onChangeTimeZone(newTimeZone.value);
}}
data-cy="time-window-picker-timezone-select"
id="time-window-picker-timezone-select"
/>
</div>
{errors?.StartTime && <FormError>{errors.StartTime}</FormError>}

View File

@@ -54,6 +54,7 @@ export function EdgeGroupsField({
closeMenuOnSelect={false}
isDisabled={disabled}
data-cy="update-schedules-edge-groups-select"
id="update-schedules-edge-groups-select"
/>
</FormControl>
<TextTip color="blue">

View File

@@ -41,6 +41,7 @@ export function CredentialSelector({
noOptionsMessage={() => 'no saved credentials'}
inputId="git-creds-selector"
data-cy="git-credentials-selector"
id="git-credentials-selector"
/>
</FormControl>
</div>

View File

@@ -70,7 +70,6 @@ export interface Registry {
BaseURL: string;
Authentication: boolean;
Username: string;
Password?: string;
RegistryAccesses: RegistryAccesses | null;
Gitlab: Gitlab;
Quay: Quay;

View File

@@ -15,7 +15,6 @@ function buildTestRegistry(
Name: name,
Username: '',
Authentication: false,
Password: '',
BaseURL: '',
Ecr: { Region: '' },
Github: { OrganisationName: '', UseOrganisation: false },

169
yarn.lock
View File

@@ -2988,11 +2988,11 @@
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.18.6":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
dependencies:
regenerator-runtime "^0.13.11"
regenerator-runtime "^0.14.0"
"@babel/template@^7.18.10", "@babel/template@^7.20.7":
version "7.20.7"
@@ -3157,84 +3157,86 @@
statuses "^2.0.1"
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.4.0":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.4.0.tgz#76ac9a2a411a4cc6e13103014dba5e0fe601da5a"
integrity sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==
version "6.18.4"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz#4394f55d6771727179f2e28a871ef46bbbeb11b1"
integrity sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.6.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0":
version "6.1.3"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.1.3.tgz#401d0b6d18e7d5eb9a96f6c8ae4ea56a08e8fd06"
integrity sha512-wUw1+vb34Ultv0Q9m/OVB7yizGXgtoDbkI5f5ErM8bebwLyUYjicdhJTKhTvPTpgkv8dq/BK0lQ3K5pRf2DAJw==
version "6.8.0"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.8.0.tgz#92f200b66f852939bd6ebb90d48c2d9e9c813d64"
integrity sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.2.0"
"@codemirror/view" "^6.0.0"
"@lezer/common" "^1.0.0"
"@codemirror/state" "^6.4.0"
"@codemirror/view" "^6.27.0"
"@lezer/common" "^1.1.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.3.2":
version "6.3.2"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.3.2.tgz#a3d5796d17a2cd3110bac0f5126db67c7e90a0f3"
integrity sha512-g42uHhOcEMAXjmozGG+rdom5UsbyfMxQFh7AbkeoaNImddL6Xt4cQDL0+JxmG7+as18rUAvZaqzP/TjsciVIrA==
version "6.10.8"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.8.tgz#3e3a346a2b0a8cf63ee1cfe03349eb1965dce5f9"
integrity sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@lezer/common" "^1.0.0"
"@codemirror/view" "^6.23.0"
"@lezer/common" "^1.1.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
"@codemirror/legacy-modes@^6.3.1":
version "6.3.1"
resolved "https://registry.yarnpkg.com/@codemirror/legacy-modes/-/legacy-modes-6.3.1.tgz#77ab3f3db1ce3e47aad7a5baac3a4b12844734a5"
integrity sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw==
version "6.4.2"
resolved "https://registry.yarnpkg.com/@codemirror/legacy-modes/-/legacy-modes-6.4.2.tgz#723a55aae21304d4c112575943d3467c9040d217"
integrity sha512-HsvWu08gOIIk303eZQCal4H4t65O/qp1V4ul4zVa3MHK5FJ0gz3qz3O55FIkm+aQUcshUOjBx38t2hPiJwW5/g==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/lint@^6.0.0", "@codemirror/lint@^6.1.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.1.0.tgz#f006142d3a580fdb8ffc2faa3361b2232c08e079"
integrity sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==
version "6.8.4"
resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.4.tgz#7d8aa5d1a6dec89ffcc23ad45ddca2e12e90982d"
integrity sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@codemirror/view" "^6.35.0"
crelt "^1.0.5"
"@codemirror/search@^6.0.0", "@codemirror/search@^6.2.3":
version "6.2.3"
resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.2.3.tgz#fab933fef1b1de8ef40cda275c73d9ac7a1ff40f"
integrity sha512-V9n9233lopQhB1dyjsBK2Wc1i+8hcCqxl1wQ46c5HWWLePoe4FluV3TGHoZ04rBRlGjNyz9DTmpJErig8UE4jw==
version "6.5.8"
resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.8.tgz#b59b3659b46184cc75d6108d7c050a4ca344c3a0"
integrity sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.0.tgz#a0fb08403ced8c2a68d1d0acee926bd20be922f2"
integrity sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.2.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0":
version "6.5.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.1.tgz#e5c0599f7b43cf03f19e05861317df5425c07904"
integrity sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@codemirror/theme-one-dark@^6.0.0", "@codemirror/theme-one-dark@^6.1.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.0.tgz#6f8b3c7fc22e9fec59edd573f4ba9546db42e007"
integrity sha512-AiTHtFRu8+vWT9wWUWDM+cog6ZwgivJogB1Tm/g40NIpLwph7AnmxrSzWfvJN5fBVufsuwBxecQCNmdcR5D7Aw==
version "6.1.2"
resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@lezer/highlight" "^1.0.0"
"@codemirror/view@^6.0.0", "@codemirror/view@^6.6.0", "@codemirror/view@^6.7.1":
version "6.7.1"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.7.1.tgz#370e95d6f001e7f5cadc459807974b4f0a6eb225"
integrity sha512-kYtS+uqYw/q/0ytYxpkqE1JVuK5NsbmBklWYhwLFTKO9gVuTdh/kDEeZPKorbqHcJ+P+ucrhcsS1czVweOpT2g==
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.7.1":
version "6.36.2"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.2.tgz#aeb644e161440734ac5a153bf6e5b4a4355047be"
integrity sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==
dependencies:
"@codemirror/state" "^6.1.4"
style-mod "^4.0.0"
"@codemirror/state" "^6.5.0"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@colors/colors@1.5.0":
@@ -3890,12 +3892,24 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@lezer/common@^1.0.0", "@lezer/common@^1.0.2":
"@lezer/common@^1.0.0", "@lezer/common@^1.1.0":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd"
integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==
"@lezer/common@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.0.2.tgz#8fb9b86bdaa2ece57e7d59e5ffbcb37d71815087"
integrity sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
"@lezer/highlight@^1.0.0":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b"
integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/highlight@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.1.3.tgz#bf5a36c2ee227f526d74997ac91f7777e29bd25d"
integrity sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==
@@ -3903,9 +3917,9 @@
"@lezer/common" "^1.0.0"
"@lezer/lr@^1.0.0":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.2.5.tgz#e9088164a711690596f17378665e0554157c9b03"
integrity sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==
version "1.4.2"
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.2.tgz#931ea3dea8e9de84e90781001dae30dea9ff1727"
integrity sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==
dependencies:
"@lezer/common" "^1.0.0"
@@ -3914,6 +3928,11 @@
resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.9.tgz#85f221eb82f9d555e180e87d6e50fb154af85408"
integrity sha512-yN599ZBuMPPK4tdoToLlvgJB4CLK8fGl7ntfy0Wn7U6ttNvHYurd81bfUiK/6sMkiIwm65R6ck4L6+Y3DfVbNQ==
"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@mdx-js/react@^2.1.5":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-2.3.0.tgz#4208bd6d70f0d0831def28ef28c26149b03180b3"
@@ -6547,10 +6566,10 @@
classnames "^2.3.1"
prop-types "^15.6.1"
"@uiw/codemirror-extensions-basic-setup@4.19.5":
version "4.19.5"
resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.19.5.tgz#2fcfa7b92236f316f0291378e9c5aaa1611146e9"
integrity sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==
"@uiw/codemirror-extensions-basic-setup@4.23.7":
version "4.23.7"
resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.7.tgz#8fce5d6190a755c889805d2edc5b85d7f29cd322"
integrity sha512-9/2EUa1Lck4kFKkR2BkxlZPpgD/EWuKHnOlysf1yHKZGraaZmZEaUw+utDK4QcuJc8Iz097vsLz4f4th5EU27g==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/commands" "^6.0.0"
@@ -6561,24 +6580,24 @@
"@codemirror/view" "^6.0.0"
"@uiw/codemirror-themes@^4.19.9":
version "4.19.9"
resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.19.9.tgz#988876213a2e350244ac2a0d479ebb792afbe94d"
integrity sha512-PH3hl1w42z7GXe/zoD9gSadOGBWyKPl7vHm/8V1PUuHXT21+neyfRc7v0xPwb05pGP6ExfbmPi78y4+g6cHopg==
version "4.23.7"
resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.7.tgz#33d09a2d9df3eda3e3affcb68d91672e41bf646a"
integrity sha512-UNf1XOx1hG9OmJnrtT86PxKcdcwhaNhbrcD+nsk8WxRJ3n5c8nH6euDvgVPdVLPwbizsaQcZTILACgA/FjRpVg==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@uiw/react-codemirror@^4.19.5":
version "4.19.5"
resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.19.5.tgz#a3fac44a741a3cbefb0fd58be4fa621e201f247e"
integrity sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==
version "4.23.7"
resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.7.tgz#b7fe2085936c593514f5e238865989bfef65e504"
integrity sha512-Nh/0P6W+kWta+ARp9YpnKPD9ick5teEnwmtNoPQnyd6NPv0EQP3Ui4YmRVNj1nkUEo+QjrAUaEfcejJ2up/HZA==
dependencies:
"@babel/runtime" "^7.18.6"
"@codemirror/commands" "^6.1.0"
"@codemirror/state" "^6.1.1"
"@codemirror/theme-one-dark" "^6.0.0"
"@uiw/codemirror-extensions-basic-setup" "4.19.5"
"@uiw/codemirror-extensions-basic-setup" "4.23.7"
codemirror "^6.0.0"
"@vitest/coverage-v8@^2.0.4":
@@ -8584,9 +8603,9 @@ cosmiconfig@^8.2.0:
path-type "^4.0.0"
crelt@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-fetch@3.1.5:
version "3.1.5"
@@ -14976,6 +14995,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"
@@ -16111,10 +16135,10 @@ style-loader@^3.3.1, style-loader@^3.3.2, style-loader@^3.3.3:
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.3.tgz#bba8daac19930169c0c9c96706749a597ae3acff"
integrity sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==
style-mod@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01"
integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==
style-mod@^4.0.0, style-mod@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
styled-components@^5.3.0:
version "5.3.3"
@@ -17169,9 +17193,9 @@ void-elements@3.1.0:
integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
w3c-keyname@^2.2.4:
version "2.2.6"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f"
integrity sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
w3c-xmlserializer@^5.0.0:
version "5.0.0"
@@ -17596,15 +17620,6 @@ wrap-ansi@^7.0.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"