Compare commits
23 Commits
release/2.
...
release/2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27cbc3818e | ||
|
|
e943aa8f03 | ||
|
|
17a4750d8e | ||
|
|
7d18c22aa1 | ||
|
|
c80cc6e268 | ||
|
|
b30a1b5250 | ||
|
|
b753371700 | ||
|
|
3ca5ab180f | ||
|
|
4971f5510c | ||
|
|
20fa7e508d | ||
|
|
ebffc340d9 | ||
|
|
9a86737caa | ||
|
|
d35d8a7307 | ||
|
|
701ff5d6bc | ||
|
|
9044b25a23 | ||
|
|
7f089fab86 | ||
|
|
a259c28678 | ||
|
|
db48da185a | ||
|
|
cab667c23b | ||
|
|
154ca9f1b1 | ||
|
|
2abe40b786 | ||
|
|
6be2420b32 | ||
|
|
9405cc0e04 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(®istries[idx], false)
|
||||
}
|
||||
|
||||
return response.JSON(w, registries)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,7 @@ const _KubernetesConfigurationFormValues = Object.freeze({
|
||||
Name: '',
|
||||
ConfigurationOwner: '',
|
||||
Kind: KubernetesConfigurationKinds.CONFIGMAP,
|
||||
kind: 'ConfigMap',
|
||||
Data: [],
|
||||
DataYaml: '',
|
||||
IsSimple: true,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
52
app/react-tools/useDebugPropChanges.ts
Normal file
52
app/react-tools/useDebugPropChanges.ts
Normal 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 */
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -104,6 +104,7 @@ export function TagSelector({
|
||||
onCreateOption={handleCreateOption}
|
||||
aria-label="Tags"
|
||||
data-cy="environment-tags-selector"
|
||||
id="environment-tags-selector"
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
|
||||
@@ -36,6 +36,7 @@ export function UsersSelector({
|
||||
onChange(selectedUsers.map((user) => user.Id))
|
||||
}
|
||||
data-cy={dataCy}
|
||||
id={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
isDisabled={disabled}
|
||||
|
||||
@@ -18,6 +18,7 @@ export function Select<T extends number | string>({
|
||||
options,
|
||||
className,
|
||||
'data-cy': dataCy,
|
||||
id,
|
||||
...props
|
||||
}: Props<T> & SelectHTMLAttributes<HTMLSelectElement>) {
|
||||
return (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ export function CreatableSelector({
|
||||
isDisabled={isLoading}
|
||||
closeMenuOnSelect={false}
|
||||
data-cy="edge-devices-assignment-selector"
|
||||
id="edge-devices-assignment-selector"
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export function GroupSelector() {
|
||||
placeholder="Select a group"
|
||||
isClearable
|
||||
data-cy="edge-devices-assignment-selector"
|
||||
id="edge-devices-assignment-selector"
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -122,6 +122,7 @@ export function AppIngressPathForm({
|
||||
onChangeIngressPath(newIngressPath);
|
||||
}}
|
||||
data-cy="k8sAppCreate-ingressPathHostSelect"
|
||||
id="k8sAppCreate-ingressPathHostSelect"
|
||||
/>
|
||||
<InputGroup.ButtonWrapper>
|
||||
<Button
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 : [];
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export function StorageAccessModeSelector({
|
||||
inputId={inputId}
|
||||
placeholder="Not configured"
|
||||
data-cy={`kubeSetup-storageAccessSelect${storageClassName}`}
|
||||
id={`kubeSetup-storageAccessSelect${storageClassName}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export function NamespacesSelector({
|
||||
onChange(selectedTeams.map((namespace) => namespace.name))
|
||||
}
|
||||
data-cy={dataCy}
|
||||
id={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function CreateFromManifestButton({
|
||||
}}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
Create from file
|
||||
Create from code
|
||||
</AddButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -35,6 +35,7 @@ export function NamespaceAccessUsersSelector({
|
||||
closeMenuOnSelect={false}
|
||||
onChange={onChange}
|
||||
data-cy={dataCy}
|
||||
id={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
components={{ MultiValueLabel, Option: OptionComponent }}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -94,7 +94,7 @@ function HelmDatatableDescription({ isAdmin }: { isAdmin: boolean }) {
|
||||
) : (
|
||||
<span>globally-set Helm repo</span>
|
||||
)}
|
||||
) and shown in the Create from file screen's Helm charts list.
|
||||
) and shown in the Create from code screen's Helm charts list.
|
||||
</TextTip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -70,7 +70,6 @@ export interface Registry {
|
||||
BaseURL: string;
|
||||
Authentication: boolean;
|
||||
Username: string;
|
||||
Password?: string;
|
||||
RegistryAccesses: RegistryAccesses | null;
|
||||
Gitlab: Gitlab;
|
||||
Quay: Quay;
|
||||
|
||||
@@ -15,7 +15,6 @@ function buildTestRegistry(
|
||||
Name: name,
|
||||
Username: '',
|
||||
Authentication: false,
|
||||
Password: '',
|
||||
BaseURL: '',
|
||||
Ecr: { Region: '' },
|
||||
Github: { OrganisationName: '', UseOrganisation: false },
|
||||
|
||||
169
yarn.lock
169
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user