mirror of https://github.com/usual2970/certimate
commit
2ed94bf509
|
@ -3,6 +3,7 @@ package applicant
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||||
|
@ -25,6 +26,7 @@ func (a *aliyun) Apply() (*Certificate, error) {
|
||||||
|
|
||||||
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
||||||
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
|
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
|
||||||
|
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||||
dnsProvider, err := alidns.NewDNSProvider()
|
dnsProvider, err := alidns.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package applicant
|
package applicant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"certimate/internal/domain"
|
||||||
"certimate/internal/utils/app"
|
"certimate/internal/utils/app"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
@ -46,6 +47,8 @@ var sslProviderUrls = map[string]string{
|
||||||
|
|
||||||
const defaultEmail = "536464346@qq.com"
|
const defaultEmail = "536464346@qq.com"
|
||||||
|
|
||||||
|
const defaultTimeout = 60
|
||||||
|
|
||||||
type Certificate struct {
|
type Certificate struct {
|
||||||
CertUrl string `json:"certUrl"`
|
CertUrl string `json:"certUrl"`
|
||||||
CertStableUrl string `json:"certStableUrl"`
|
CertStableUrl string `json:"certStableUrl"`
|
||||||
|
@ -60,6 +63,7 @@ type ApplyOption struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Access string `json:"access"`
|
Access string `json:"access"`
|
||||||
Nameservers string `json:"nameservers"`
|
Nameservers string `json:"nameservers"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MyUser struct {
|
type MyUser struct {
|
||||||
|
@ -83,8 +87,22 @@ type Applicant interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(record *models.Record) (Applicant, error) {
|
func Get(record *models.Record) (Applicant, error) {
|
||||||
access := record.ExpandedOne("access")
|
|
||||||
email := record.GetString("email")
|
if record.GetString("applyConfig") == "" {
|
||||||
|
return nil, errors.New("apply config is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig := &domain.ApplyConfig{}
|
||||||
|
|
||||||
|
record.UnmarshalJSONField("applyConfig", applyConfig)
|
||||||
|
|
||||||
|
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("access record not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
email := applyConfig.Email
|
||||||
if email == "" {
|
if email == "" {
|
||||||
email = defaultEmail
|
email = defaultEmail
|
||||||
}
|
}
|
||||||
|
@ -92,7 +110,8 @@ func Get(record *models.Record) (Applicant, error) {
|
||||||
Email: email,
|
Email: email,
|
||||||
Domain: record.GetString("domain"),
|
Domain: record.GetString("domain"),
|
||||||
Access: access.GetString("config"),
|
Access: access.GetString("config"),
|
||||||
Nameservers: record.GetString("nameservers"),
|
Nameservers: applyConfig.Nameservers,
|
||||||
|
Timeout: applyConfig.Timeout,
|
||||||
}
|
}
|
||||||
switch access.GetString("configType") {
|
switch access.GetString("configType") {
|
||||||
case configTypeAliyun:
|
case configTypeAliyun:
|
||||||
|
|
|
@ -3,6 +3,7 @@ package applicant
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||||
|
@ -23,6 +24,7 @@ func (c *cloudflare) Apply() (*Certificate, error) {
|
||||||
json.Unmarshal([]byte(c.option.Access), access)
|
json.Unmarshal([]byte(c.option.Access), access)
|
||||||
|
|
||||||
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
||||||
|
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
|
||||||
|
|
||||||
provider, err := cf.NewDNSProvider()
|
provider, err := cf.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package applicant
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||||
|
@ -25,6 +26,7 @@ func (a *godaddy) Apply() (*Certificate, error) {
|
||||||
|
|
||||||
os.Setenv("GODADDY_API_KEY", access.ApiKey)
|
os.Setenv("GODADDY_API_KEY", access.ApiKey)
|
||||||
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
|
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
|
||||||
|
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package applicant
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||||
|
@ -26,6 +27,8 @@ func (t *huaweicloud) Apply() (*Certificate, error) {
|
||||||
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
||||||
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
|
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
|
||||||
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
||||||
|
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
|
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -3,6 +3,7 @@ package applicant
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||||
|
@ -24,6 +25,7 @@ func (a *namesilo) Apply() (*Certificate, error) {
|
||||||
json.Unmarshal([]byte(a.option.Access), access)
|
json.Unmarshal([]byte(a.option.Access), access)
|
||||||
|
|
||||||
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
||||||
|
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package applicant
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||||
|
@ -25,6 +26,8 @@ func (t *tencent) Apply() (*Certificate, error) {
|
||||||
|
|
||||||
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
||||||
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
|
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
|
||||||
|
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := tencentcloud.NewDNSProvider()
|
dnsProvider, err := tencentcloud.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -190,7 +190,7 @@ func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, er
|
||||||
|
|
||||||
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
|
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
|
||||||
CloudProduct: tea.String(a.option.Product),
|
CloudProduct: tea.String(a.option.Product),
|
||||||
Keyword: tea.String(a.option.Domain),
|
Keyword: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
|
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package deployer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
|
"certimate/internal/utils/rand"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -46,9 +47,9 @@ func (a *AliyunCdn) GetInfo() []string {
|
||||||
|
|
||||||
func (a *AliyunCdn) Deploy(ctx context.Context) error {
|
func (a *AliyunCdn) Deploy(ctx context.Context) error {
|
||||||
|
|
||||||
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
|
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
|
||||||
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
|
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
|
||||||
DomainName: tea.String(a.option.Domain),
|
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||||
CertName: tea.String(certName),
|
CertName: tea.String(certName),
|
||||||
CertType: tea.String("upload"),
|
CertType: tea.String("upload"),
|
||||||
SSLProtocol: tea.String("on"),
|
SSLProtocol: tea.String("on"),
|
||||||
|
|
|
@ -7,6 +7,7 @@ package deployer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
|
"certimate/internal/utils/rand"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -51,9 +52,9 @@ func (a *AliyunEsa) GetInfo() []string {
|
||||||
|
|
||||||
func (a *AliyunEsa) Deploy(ctx context.Context) error {
|
func (a *AliyunEsa) Deploy(ctx context.Context) error {
|
||||||
|
|
||||||
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
|
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
|
||||||
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
|
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
|
||||||
DomainName: tea.String(a.option.Domain),
|
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||||
CertName: tea.String(certName),
|
CertName: tea.String(certName),
|
||||||
CertType: tea.String("upload"),
|
CertType: tea.String("upload"),
|
||||||
SSLProtocol: tea.String("on"),
|
SSLProtocol: tea.String("on"),
|
||||||
|
|
|
@ -2,8 +2,8 @@ package deployer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"certimate/internal/applicant"
|
"certimate/internal/applicant"
|
||||||
|
"certimate/internal/domain"
|
||||||
"certimate/internal/utils/app"
|
"certimate/internal/utils/app"
|
||||||
"certimate/internal/utils/variables"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -30,6 +30,7 @@ type DeployerOption struct {
|
||||||
Product string `json:"product"`
|
Product string `json:"product"`
|
||||||
Access string `json:"access"`
|
Access string `json:"access"`
|
||||||
AceessRecord *models.Record `json:"-"`
|
AceessRecord *models.Record `json:"-"`
|
||||||
|
DeployConfig domain.DeployConfig `json:"deployConfig"`
|
||||||
Certificate applicant.Certificate `json:"certificate"`
|
Certificate applicant.Certificate `json:"certificate"`
|
||||||
Variables map[string]string `json:"variables"`
|
Variables map[string]string `json:"variables"`
|
||||||
}
|
}
|
||||||
|
@ -42,52 +43,29 @@ type Deployer interface {
|
||||||
|
|
||||||
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
||||||
rs := make([]Deployer, 0)
|
rs := make([]Deployer, 0)
|
||||||
|
if record.GetString("deployConfig") == "" {
|
||||||
if record.GetString("targetAccess") != "" {
|
return rs, nil
|
||||||
singleDeployer, err := Get(record, cert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rs = append(rs, singleDeployer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if record.GetString("group") != "" {
|
deployConfigs := make([]domain.DeployConfig, 0)
|
||||||
group := record.ExpandedOne("group")
|
|
||||||
|
|
||||||
if errs := app.GetApp().Dao().ExpandRecord(group, []string{"access"}, nil); len(errs) > 0 {
|
|
||||||
|
|
||||||
errList := make([]error, 0)
|
|
||||||
for name, err := range errs {
|
|
||||||
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
|
|
||||||
}
|
|
||||||
err := errors.Join(errList...)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
records := group.ExpandedAll("access")
|
|
||||||
|
|
||||||
deployers, err := getByGroup(record, cert, records...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rs = append(rs, deployers...)
|
|
||||||
|
|
||||||
|
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析部署配置失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rs, nil
|
if len(deployConfigs) == 0 {
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
}
|
for _, deployConfig := range deployConfigs {
|
||||||
|
|
||||||
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
|
deployer, err := getWithDeployConfig(record, cert, deployConfig)
|
||||||
|
|
||||||
rs := make([]Deployer, 0)
|
|
||||||
|
|
||||||
for _, access := range accesses {
|
|
||||||
deployer, err := getWithAccess(record, cert, access)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rs = append(rs, deployer)
|
rs = append(rs, deployer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,15 +73,21 @@ func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWithAccess(record *models.Record, cert *applicant.Certificate, access *models.Record) (Deployer, error) {
|
func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) {
|
||||||
|
|
||||||
|
access, err := app.GetApp().Dao().FindRecordById("access", deployConfig.Access)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("access record not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
option := &DeployerOption{
|
option := &DeployerOption{
|
||||||
DomainId: record.Id,
|
DomainId: record.Id,
|
||||||
Domain: record.GetString("domain"),
|
Domain: record.GetString("domain"),
|
||||||
Product: getProduct(record),
|
Product: getProduct(deployConfig.Type),
|
||||||
Access: access.GetString("config"),
|
Access: access.GetString("config"),
|
||||||
AceessRecord: access,
|
AceessRecord: access,
|
||||||
Variables: variables.Parse2Map(record.GetString("variables")),
|
DeployConfig: deployConfig,
|
||||||
}
|
}
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
option.Certificate = *cert
|
option.Certificate = *cert
|
||||||
|
@ -114,7 +98,7 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch record.GetString("targetType") {
|
switch deployConfig.Type {
|
||||||
case targetAliyunOss:
|
case targetAliyunOss:
|
||||||
return NewAliyun(option)
|
return NewAliyun(option)
|
||||||
case targetAliyunCdn:
|
case targetAliyunCdn:
|
||||||
|
@ -136,16 +120,8 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
func getProduct(t string) string {
|
||||||
|
rs := strings.Split(t, "-")
|
||||||
access := record.ExpandedOne("targetAccess")
|
|
||||||
|
|
||||||
return getWithAccess(record, cert, access)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProduct(record *models.Record) string {
|
|
||||||
targetType := record.GetString("targetType")
|
|
||||||
rs := strings.Split(targetType, "-")
|
|
||||||
if len(rs) < 2 {
|
if len(rs) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -159,3 +135,39 @@ func toStr(tag string, data any) string {
|
||||||
byts, _ := json.Marshal(data)
|
byts, _ := json.Marshal(data)
|
||||||
return tag + ":" + string(byts)
|
return tag + ":" + string(byts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDeployString(conf domain.DeployConfig, key string) string {
|
||||||
|
if _, ok := conf.Config[key]; !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := conf.Config[key].(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeployVariables(conf domain.DeployConfig) map[string]string {
|
||||||
|
rs := make(map[string]string)
|
||||||
|
data, ok := conf.Config["variables"]
|
||||||
|
if !ok {
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
bts, _ := json.Marshal(data)
|
||||||
|
|
||||||
|
kvData := make([]domain.KV, 0)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(bts, &kvData); err != nil {
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kv := range kvData {
|
||||||
|
rs[kv.Key] = kv.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return rs
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -11,9 +11,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type localAccess struct {
|
type localAccess struct {
|
||||||
Command string `json:"command"`
|
|
||||||
CertPath string `json:"certPath"`
|
|
||||||
KeyPath string `json:"keyPath"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type local struct {
|
type local struct {
|
||||||
|
@ -41,18 +38,27 @@ func (l *local) Deploy(ctx context.Context) error {
|
||||||
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
|
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preCommand := getDeployString(l.option.DeployConfig, "preCommand")
|
||||||
|
|
||||||
|
if preCommand != "" {
|
||||||
|
if err := execCmd(preCommand); err != nil {
|
||||||
|
return fmt.Errorf("执行前置命令失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 复制文件
|
// 复制文件
|
||||||
if err := copyFile(l.option.Certificate.Certificate, access.CertPath); err != nil {
|
if err := copyFile(l.option.Certificate.Certificate, getDeployString(l.option.DeployConfig, "certPath")); err != nil {
|
||||||
return fmt.Errorf("复制证书失败: %w", err)
|
return fmt.Errorf("复制证书失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := copyFile(l.option.Certificate.PrivateKey, access.KeyPath); err != nil {
|
if err := copyFile(l.option.Certificate.PrivateKey, getDeployString(l.option.DeployConfig, "keyPath")); err != nil {
|
||||||
return fmt.Errorf("复制私钥失败: %w", err)
|
return fmt.Errorf("复制私钥失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行命令
|
// 执行命令
|
||||||
|
|
||||||
if err := execCmd(access.Command); err != nil {
|
if err := execCmd(getDeployString(l.option.DeployConfig, "command")); err != nil {
|
||||||
return fmt.Errorf("执行命令失败: %w", err)
|
return fmt.Errorf("执行命令失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ func (q *qiuniu) Deploy(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *qiuniu) enableHttps(certId string) error {
|
func (q *qiuniu) enableHttps(certId string) error {
|
||||||
path := fmt.Sprintf("/domain/%s/sslize", q.option.Domain)
|
path := fmt.Sprintf("/domain/%s/sslize", getDeployString(q.option.DeployConfig, "domain"))
|
||||||
|
|
||||||
body := &modifyDomainCertReq{
|
body := &modifyDomainCertReq{
|
||||||
CertID: certId,
|
CertID: certId,
|
||||||
|
@ -104,7 +104,7 @@ type domainInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
|
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
|
||||||
path := fmt.Sprintf("/domain/%s", q.option.Domain)
|
path := fmt.Sprintf("/domain/%s", getDeployString(q.option.DeployConfig, "domain"))
|
||||||
|
|
||||||
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
|
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -135,8 +135,8 @@ func (q *qiuniu) uploadCert() (string, error) {
|
||||||
path := "/sslcert"
|
path := "/sslcert"
|
||||||
|
|
||||||
body := &uploadCertReq{
|
body := &uploadCertReq{
|
||||||
Name: q.option.Domain,
|
Name: getDeployString(q.option.DeployConfig, "domain"),
|
||||||
CommonName: q.option.Domain,
|
CommonName: getDeployString(q.option.DeployConfig, "domain"),
|
||||||
Pri: q.option.Certificate.PrivateKey,
|
Pri: q.option.Certificate.PrivateKey,
|
||||||
Ca: q.option.Certificate.Certificate,
|
Ca: q.option.Certificate.Certificate,
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,7 @@ type modifyDomainCertReq struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *qiuniu) modifyDomainCert(certId string) error {
|
func (q *qiuniu) modifyDomainCert(certId string) error {
|
||||||
path := fmt.Sprintf("/domain/%s/httpsconf", q.option.Domain)
|
path := fmt.Sprintf("/domain/%s/httpsconf", getDeployString(q.option.DeployConfig, "domain"))
|
||||||
|
|
||||||
body := &modifyDomainCertReq{
|
body := &modifyDomainCertReq{
|
||||||
CertID: certId,
|
CertID: certId,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
xpath "path"
|
xpath "path"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
sshPkg "golang.org/x/crypto/ssh"
|
sshPkg "golang.org/x/crypto/ssh"
|
||||||
|
@ -19,15 +18,11 @@ type ssh struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshAccess struct {
|
type sshAccess struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Port string `json:"port"`
|
Port string `json:"port"`
|
||||||
PreCommand string `json:"preCommand"`
|
|
||||||
Command string `json:"command"`
|
|
||||||
CertPath string `json:"certPath"`
|
|
||||||
KeyPath string `json:"keyPath"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSSH(option *DeployerOption) (Deployer, error) {
|
func NewSSH(option *DeployerOption) (Deployer, error) {
|
||||||
|
@ -50,16 +45,6 @@ func (s *ssh) Deploy(ctx context.Context) error {
|
||||||
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
|
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将证书路径和命令中的变量替换为实际值
|
|
||||||
for k, v := range s.option.Variables {
|
|
||||||
key := fmt.Sprintf("${%s}", k)
|
|
||||||
access.CertPath = strings.ReplaceAll(access.CertPath, key, v)
|
|
||||||
access.KeyPath = strings.ReplaceAll(access.KeyPath, key, v)
|
|
||||||
access.Command = strings.ReplaceAll(access.Command, key, v)
|
|
||||||
access.PreCommand = strings.ReplaceAll(access.PreCommand, key, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接
|
// 连接
|
||||||
client, err := s.getClient(access)
|
client, err := s.getClient(access)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -70,29 +55,30 @@ func (s *ssh) Deploy(ctx context.Context) error {
|
||||||
s.infos = append(s.infos, toStr("ssh连接成功", nil))
|
s.infos = append(s.infos, toStr("ssh连接成功", nil))
|
||||||
|
|
||||||
// 执行前置命令
|
// 执行前置命令
|
||||||
if access.PreCommand != "" {
|
preCommand := getDeployString(s.option.DeployConfig, "preCommand")
|
||||||
err, stdout, stderr := s.sshExecCommand(client, access.PreCommand)
|
if preCommand != "" {
|
||||||
|
err, stdout, stderr := s.sshExecCommand(client, preCommand)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传证书
|
// 上传证书
|
||||||
if err := s.upload(client, s.option.Certificate.Certificate, access.CertPath); err != nil {
|
if err := s.upload(client, s.option.Certificate.Certificate, getDeployString(s.option.DeployConfig, "certPath")); err != nil {
|
||||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
|
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
|
||||||
|
|
||||||
// 上传私钥
|
// 上传私钥
|
||||||
if err := s.upload(client, s.option.Certificate.PrivateKey, access.KeyPath); err != nil {
|
if err := s.upload(client, s.option.Certificate.PrivateKey, getDeployString(s.option.DeployConfig, "keyPath")); err != nil {
|
||||||
return fmt.Errorf("failed to upload private key: %w", err)
|
return fmt.Errorf("failed to upload private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
|
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
|
||||||
|
|
||||||
// 执行命令
|
// 执行命令
|
||||||
err, stdout, stderr := s.sshExecCommand(client, access.Command)
|
err, stdout, stderr := s.sshExecCommand(client, getDeployString(s.option.DeployConfig, "command"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,10 @@ type webhookAccess struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type hookData struct {
|
type hookData struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"`
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"`
|
||||||
|
Variables map[string]string `json:"variables"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type webhook struct {
|
type webhook struct {
|
||||||
|
@ -50,6 +51,7 @@ func (w *webhook) Deploy(ctx context.Context) error {
|
||||||
Domain: w.option.Domain,
|
Domain: w.option.Domain,
|
||||||
Certificate: w.option.Certificate.Certificate,
|
Certificate: w.option.Certificate.Certificate,
|
||||||
PrivateKey: w.option.Certificate.PrivateKey,
|
PrivateKey: w.option.Certificate.PrivateKey,
|
||||||
|
Variables: getDeployVariables(w.option.DeployConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := json.Marshal(data)
|
body, _ := json.Marshal(data)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type ApplyConfig struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Access string `json:"access"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
|
Nameservers string `json:"nameservers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeployConfig struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Access string `json:"access"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Config map[string]any `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"certimate/internal/deployer"
|
"certimate/internal/deployer"
|
||||||
"certimate/internal/utils/app"
|
"certimate/internal/utils/app"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -41,18 +40,6 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
history.record(checkPhase, "获取记录成功", nil)
|
history.record(checkPhase, "获取记录成功", nil)
|
||||||
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess", "group"}, nil); len(errs) > 0 {
|
|
||||||
|
|
||||||
errList := make([]error, 0)
|
|
||||||
for name, err := range errs {
|
|
||||||
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
|
|
||||||
}
|
|
||||||
err = errors.Join(errList...)
|
|
||||||
app.GetApp().Logger().Error("展开记录失败", "err", err)
|
|
||||||
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
history.record(checkPhase, "获取授权信息成功", nil)
|
|
||||||
|
|
||||||
cert := currRecord.GetString("certificate")
|
cert := currRecord.GetString("certificate")
|
||||||
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
||||||
|
@ -106,6 +93,13 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 没有部署配置,也算成功
|
||||||
|
if len(deployers) == 0 {
|
||||||
|
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
|
||||||
|
history.setWholeSuccess(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, deployer := range deployers {
|
for _, deployer := range deployers {
|
||||||
if err = deployer.Deploy(ctx); err != nil {
|
if err = deployer.Deploy(ctx); err != nil {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,731 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
jsonData := `[
|
||||||
|
{
|
||||||
|
"id": "z3p974ainxjqlvs",
|
||||||
|
"created": "2024-07-29 10:02:48.334Z",
|
||||||
|
"updated": "2024-10-08 06:50:56.637Z",
|
||||||
|
"name": "domains",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "iuaerpl2",
|
||||||
|
"name": "domain",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ukkhuw85",
|
||||||
|
"name": "email",
|
||||||
|
"type": "email",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"exceptDomains": null,
|
||||||
|
"onlyDomains": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "v98eebqq",
|
||||||
|
"name": "crontab",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "alc8e9ow",
|
||||||
|
"name": "access",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "4yzbv8urny5ja1e",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "topsc9bj",
|
||||||
|
"name": "certUrl",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "vixgq072",
|
||||||
|
"name": "certStableUrl",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "g3a3sza5",
|
||||||
|
"name": "privateKey",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "gr6iouny",
|
||||||
|
"name": "certificate",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "tk6vnrmn",
|
||||||
|
"name": "issuerCertificate",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "sjo6ibse",
|
||||||
|
"name": "csr",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "x03n1bkj",
|
||||||
|
"name": "expiredAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "srybpixz",
|
||||||
|
"name": "targetType",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"aliyun-oss",
|
||||||
|
"aliyun-cdn",
|
||||||
|
"aliyun-dcdn",
|
||||||
|
"ssh",
|
||||||
|
"webhook",
|
||||||
|
"tencent-cdn",
|
||||||
|
"qiniu-cdn",
|
||||||
|
"local"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "xy7yk0mb",
|
||||||
|
"name": "targetAccess",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "4yzbv8urny5ja1e",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "6jqeyggw",
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hdsjcchf",
|
||||||
|
"name": "deployed",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "aiya3rev",
|
||||||
|
"name": "rightnow",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ixznmhzc",
|
||||||
|
"name": "lastDeployedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ghtlkn5j",
|
||||||
|
"name": "lastDeployment",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "0a1o4e6sstp694f",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "zfnyj9he",
|
||||||
|
"name": "variables",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "1bspzuku",
|
||||||
|
"name": "group",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "teolp9pl72dxlxq",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "g65gfh7a",
|
||||||
|
"name": "nameservers",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "wwrzc3jo",
|
||||||
|
"name": "applyConfig",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "474iwy8r",
|
||||||
|
"name": "deployConfig",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4yzbv8urny5ja1e",
|
||||||
|
"created": "2024-07-29 10:04:39.685Z",
|
||||||
|
"updated": "2024-10-11 13:55:13.777Z",
|
||||||
|
"name": "access",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "geeur58v",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "iql7jpwx",
|
||||||
|
"name": "config",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hwy7m03o",
|
||||||
|
"name": "configType",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"aliyun",
|
||||||
|
"tencent",
|
||||||
|
"huaweicloud",
|
||||||
|
"qiniu",
|
||||||
|
"cloudflare",
|
||||||
|
"namesilo",
|
||||||
|
"godaddy",
|
||||||
|
"local",
|
||||||
|
"ssh",
|
||||||
|
"webhook"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "lr33hiwg",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hsxcnlvd",
|
||||||
|
"name": "usage",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"apply",
|
||||||
|
"deploy",
|
||||||
|
"all"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "c8egzzwj",
|
||||||
|
"name": "group",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "teolp9pl72dxlxq",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0a1o4e6sstp694f",
|
||||||
|
"created": "2024-07-30 06:30:27.801Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "deployments",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "farvlzk7",
|
||||||
|
"name": "domain",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "z3p974ainxjqlvs",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "jx5f69i3",
|
||||||
|
"name": "log",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "qbxdtg9q",
|
||||||
|
"name": "phase",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"check",
|
||||||
|
"apply",
|
||||||
|
"deploy"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "rglrp1hz",
|
||||||
|
"name": "phaseSuccess",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "lt1g1blu",
|
||||||
|
"name": "deployedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "wledpzgb",
|
||||||
|
"name": "wholeSuccess",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "_pb_users_auth_",
|
||||||
|
"created": "2024-09-12 13:09:54.234Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "users",
|
||||||
|
"type": "auth",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_name",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_avatar",
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp"
|
||||||
|
],
|
||||||
|
"thumbs": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "id = @request.auth.id",
|
||||||
|
"viewRule": "id = @request.auth.id",
|
||||||
|
"createRule": "",
|
||||||
|
"updateRule": "id = @request.auth.id",
|
||||||
|
"deleteRule": "id = @request.auth.id",
|
||||||
|
"options": {
|
||||||
|
"allowEmailAuth": true,
|
||||||
|
"allowOAuth2Auth": true,
|
||||||
|
"allowUsernameAuth": true,
|
||||||
|
"exceptEmailDomains": null,
|
||||||
|
"manageRule": null,
|
||||||
|
"minPasswordLength": 8,
|
||||||
|
"onlyEmailDomains": null,
|
||||||
|
"onlyVerified": false,
|
||||||
|
"requireEmail": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dy6ccjb60spfy6p",
|
||||||
|
"created": "2024-09-12 23:12:21.677Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "settings",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "1tcmdsdf",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "f9wyhypi",
|
||||||
|
"name": "content",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "teolp9pl72dxlxq",
|
||||||
|
"created": "2024-09-13 12:51:05.611Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "access_groups",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "7sajiv6i",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "xp8admif",
|
||||||
|
"name": "access",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "4yzbv8urny5ja1e",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_RgRXp0R` + "`" + ` ON ` + "`" + `access_groups` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
]`
|
||||||
|
|
||||||
|
collections := []*models.Collection{}
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return daos.New(db).ImportCollections(collections, true, nil)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "certimate",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Michel Weststrate
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
module.exports = require('./immer.cjs.production.js')
|
||||||
|
} else {
|
||||||
|
module.exports = require('./immer.cjs.development.js')
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export interface Patch {
|
||||||
|
op: "replace" | "remove" | "add";
|
||||||
|
path: (string | number)[];
|
||||||
|
value?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void
|
||||||
|
|
||||||
|
type Base = {...} | Array<any>
|
||||||
|
interface IProduce {
|
||||||
|
/**
|
||||||
|
* Immer takes a state, and runs a function against it.
|
||||||
|
* That function can freely mutate the state, as it will create copies-on-write.
|
||||||
|
* This means that the original state will stay unchanged, and once the function finishes, the modified state is returned.
|
||||||
|
*
|
||||||
|
* If the first argument is a function, this is interpreted as the recipe, and will create a curried function that will execute the recipe
|
||||||
|
* any time it is called with the current state.
|
||||||
|
*
|
||||||
|
* @param currentState - the state to start with
|
||||||
|
* @param recipe - function that receives a proxy of the current state as first argument and which can be freely modified
|
||||||
|
* @param initialState - if a curried function is created and this argument was given, it will be used as fallback if the curried function is called with a state of undefined
|
||||||
|
* @returns The next state: a new state, or the current state if nothing was modified
|
||||||
|
*/
|
||||||
|
<S: Base>(
|
||||||
|
currentState: S,
|
||||||
|
recipe: (draftState: S) => S | void,
|
||||||
|
patchListener?: PatchListener
|
||||||
|
): S;
|
||||||
|
// curried invocations with initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void,
|
||||||
|
initialState: S
|
||||||
|
): (currentState: S | void, a: A, b: B, c: C, ...extraArgs: any[]) => S;
|
||||||
|
// curried invocations without initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void
|
||||||
|
): (currentState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProduceWithPatches {
|
||||||
|
/**
|
||||||
|
* Like `produce`, but instead of just returning the new state,
|
||||||
|
* a tuple is returned with [nextState, patches, inversePatches]
|
||||||
|
*
|
||||||
|
* Like produce, this function supports currying
|
||||||
|
*/
|
||||||
|
<S: Base>(
|
||||||
|
currentState: S,
|
||||||
|
recipe: (draftState: S) => S | void
|
||||||
|
): [S, Patch[], Patch[]];
|
||||||
|
// curried invocations with initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void,
|
||||||
|
initialState: S
|
||||||
|
): (currentState: S | void, a: A, b: B, c: C, ...extraArgs: any[]) => [S, Patch[], Patch[]];
|
||||||
|
// curried invocations without initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void
|
||||||
|
): (currentState: S, a: A, b: B, c: C, ...extraArgs: any[]) => [S, Patch[], Patch[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare export var produce: IProduce
|
||||||
|
|
||||||
|
declare export var produceWithPatches: IProduceWithPatches
|
||||||
|
|
||||||
|
declare export var nothing: typeof undefined
|
||||||
|
|
||||||
|
declare export var immerable: Symbol
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically freezes any state trees generated by immer.
|
||||||
|
* This protects against accidental modifications of the state tree outside of an immer function.
|
||||||
|
* This comes with a performance impact, so it is recommended to disable this option in production.
|
||||||
|
* By default it is turned on during local development, and turned off in production.
|
||||||
|
*/
|
||||||
|
declare export function setAutoFreeze(autoFreeze: boolean): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass false to disable strict shallow copy.
|
||||||
|
*
|
||||||
|
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
||||||
|
*/
|
||||||
|
declare export function setUseStrictShallowCopy(useStrictShallowCopy: boolean): void
|
||||||
|
|
||||||
|
declare export function applyPatches<S>(state: S, patches: Patch[]): S
|
||||||
|
|
||||||
|
declare export function original<S>(value: S): S
|
||||||
|
|
||||||
|
declare export function current<S>(value: S): S
|
||||||
|
|
||||||
|
declare export function isDraft(value: any): boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mutable draft from an (immutable) object / array.
|
||||||
|
* The draft can be modified until `finishDraft` is called
|
||||||
|
*/
|
||||||
|
declare export function createDraft<T>(base: T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a draft that was created using `createDraft`,
|
||||||
|
* finalizes the draft into a new immutable object.
|
||||||
|
* Optionally a patch-listener can be provided to gather the patches that are needed to construct the object.
|
||||||
|
*/
|
||||||
|
declare export function finishDraft<T>(base: T, listener?: PatchListener): T
|
||||||
|
|
||||||
|
declare export function enableMapSet(): void
|
||||||
|
declare export function enablePatches(): void
|
||||||
|
|
||||||
|
declare export function freeze<T>(obj: T, freeze?: boolean): T
|
|
@ -0,0 +1,262 @@
|
||||||
|
/**
|
||||||
|
* The sentinel value returned by producers to replace the draft with undefined.
|
||||||
|
*/
|
||||||
|
declare const NOTHING: unique symbol;
|
||||||
|
/**
|
||||||
|
* To let Immer treat your class instances as plain immutable objects
|
||||||
|
* (albeit with a custom prototype), you must define either an instance property
|
||||||
|
* or a static property on each of your custom classes.
|
||||||
|
*
|
||||||
|
* Otherwise, your class instance will never be drafted, which means it won't be
|
||||||
|
* safe to mutate in a produce callback.
|
||||||
|
*/
|
||||||
|
declare const DRAFTABLE: unique symbol;
|
||||||
|
|
||||||
|
type AnyFunc = (...args: any[]) => any;
|
||||||
|
type PrimitiveType = number | string | boolean;
|
||||||
|
/** Object types that should never be mapped */
|
||||||
|
type AtomicObject = Function | Promise<any> | Date | RegExp;
|
||||||
|
/**
|
||||||
|
* If the lib "ES2015.Collection" is not included in tsconfig.json,
|
||||||
|
* types like ReadonlyArray, WeakMap etc. fall back to `any` (specified nowhere)
|
||||||
|
* or `{}` (from the node types), in both cases entering an infinite recursion in
|
||||||
|
* pattern matching type mappings
|
||||||
|
* This type can be used to cast these types to `void` in these cases.
|
||||||
|
*/
|
||||||
|
type IfAvailable<T, Fallback = void> = true | false extends (T extends never ? true : false) ? Fallback : keyof T extends never ? Fallback : T;
|
||||||
|
/**
|
||||||
|
* These should also never be mapped but must be tested after regular Map and
|
||||||
|
* Set
|
||||||
|
*/
|
||||||
|
type WeakReferences = IfAvailable<WeakMap<any, any>> | IfAvailable<WeakSet<any>>;
|
||||||
|
type WritableDraft<T> = {
|
||||||
|
-readonly [K in keyof T]: Draft<T[K]>;
|
||||||
|
};
|
||||||
|
/** Convert a readonly type into a mutable type, if possible */
|
||||||
|
type Draft<T> = T extends PrimitiveType ? T : T extends AtomicObject ? T : T extends ReadonlyMap<infer K, infer V> ? Map<Draft<K>, Draft<V>> : T extends ReadonlySet<infer V> ? Set<Draft<V>> : T extends WeakReferences ? T : T extends object ? WritableDraft<T> : T;
|
||||||
|
/** Convert a mutable type into a readonly type */
|
||||||
|
type Immutable<T> = T extends PrimitiveType ? T : T extends AtomicObject ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<Immutable<K>, Immutable<V>> : T extends ReadonlySet<infer V> ? ReadonlySet<Immutable<V>> : T extends WeakReferences ? T : T extends object ? {
|
||||||
|
readonly [K in keyof T]: Immutable<T[K]>;
|
||||||
|
} : T;
|
||||||
|
interface Patch {
|
||||||
|
op: "replace" | "remove" | "add";
|
||||||
|
path: (string | number)[];
|
||||||
|
value?: any;
|
||||||
|
}
|
||||||
|
type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void;
|
||||||
|
/**
|
||||||
|
* Utility types
|
||||||
|
*/
|
||||||
|
type PatchesTuple<T> = readonly [T, Patch[], Patch[]];
|
||||||
|
type ValidRecipeReturnType<State> = State | void | undefined | (State extends undefined ? typeof NOTHING : never);
|
||||||
|
type ReturnTypeWithPatchesIfNeeded<State, UsePatches extends boolean> = UsePatches extends true ? PatchesTuple<State> : State;
|
||||||
|
/**
|
||||||
|
* Core Producer inference
|
||||||
|
*/
|
||||||
|
type InferRecipeFromCurried<Curried> = Curried extends (base: infer State, ...rest: infer Args) => any ? ReturnType<Curried> extends State ? (draft: Draft<State>, ...rest: Args) => ValidRecipeReturnType<Draft<State>> : never : never;
|
||||||
|
type InferInitialStateFromCurried<Curried> = Curried extends (base: infer State, ...rest: any[]) => any ? State : never;
|
||||||
|
type InferCurriedFromRecipe<Recipe, UsePatches extends boolean> = Recipe extends (draft: infer DraftState, ...args: infer RestArgs) => any ? ReturnType<Recipe> extends ValidRecipeReturnType<DraftState> ? (base: Immutable<DraftState>, ...args: RestArgs) => ReturnTypeWithPatchesIfNeeded<DraftState, UsePatches> : never : never;
|
||||||
|
type InferCurriedFromInitialStateAndRecipe<State, Recipe, UsePatches extends boolean> = Recipe extends (draft: Draft<State>, ...rest: infer RestArgs) => ValidRecipeReturnType<State> ? (base?: State | undefined, ...args: RestArgs) => ReturnTypeWithPatchesIfNeeded<State, UsePatches> : never;
|
||||||
|
/**
|
||||||
|
* The `produce` function takes a value and a "recipe function" (whose
|
||||||
|
* return value often depends on the base state). The recipe function is
|
||||||
|
* free to mutate its first argument however it wants. All mutations are
|
||||||
|
* only ever applied to a __copy__ of the base state.
|
||||||
|
*
|
||||||
|
* Pass only a function to create a "curried producer" which relieves you
|
||||||
|
* from passing the recipe function every time.
|
||||||
|
*
|
||||||
|
* Only plain objects and arrays are made mutable. All other objects are
|
||||||
|
* considered uncopyable.
|
||||||
|
*
|
||||||
|
* Note: This function is __bound__ to its `Immer` instance.
|
||||||
|
*
|
||||||
|
* @param {any} base - the initial state
|
||||||
|
* @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
|
||||||
|
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||||||
|
* @returns {any} a new state, or the initial state if nothing was modified
|
||||||
|
*/
|
||||||
|
interface IProduce {
|
||||||
|
/** Curried producer that infers the recipe from the curried output function (e.g. when passing to setState) */
|
||||||
|
<Curried>(recipe: InferRecipeFromCurried<Curried>, initialState?: InferInitialStateFromCurried<Curried>): Curried;
|
||||||
|
/** Curried producer that infers curried from the recipe */
|
||||||
|
<Recipe extends AnyFunc>(recipe: Recipe): InferCurriedFromRecipe<Recipe, false>;
|
||||||
|
/** Curried producer that infers curried from the State generic, which is explicitly passed in. */
|
||||||
|
<State>(recipe: (state: Draft<State>, initialState: State) => ValidRecipeReturnType<State>): (state?: State) => State;
|
||||||
|
<State, Args extends any[]>(recipe: (state: Draft<State>, ...args: Args) => ValidRecipeReturnType<State>, initialState: State): (state?: State, ...args: Args) => State;
|
||||||
|
<State>(recipe: (state: Draft<State>) => ValidRecipeReturnType<State>): (state: State) => State;
|
||||||
|
<State, Args extends any[]>(recipe: (state: Draft<State>, ...args: Args) => ValidRecipeReturnType<State>): (state: State, ...args: Args) => State;
|
||||||
|
/** Curried producer with initial state, infers recipe from initial state */
|
||||||
|
<State, Recipe extends Function>(recipe: Recipe, initialState: State): InferCurriedFromInitialStateAndRecipe<State, Recipe, false>;
|
||||||
|
/** Normal producer */
|
||||||
|
<Base, D = Draft<Base>>(// By using a default inferred D, rather than Draft<Base> in the recipe, we can override it.
|
||||||
|
base: Base, recipe: (draft: D) => ValidRecipeReturnType<D>, listener?: PatchListener): Base;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Like `produce`, but instead of just returning the new state,
|
||||||
|
* a tuple is returned with [nextState, patches, inversePatches]
|
||||||
|
*
|
||||||
|
* Like produce, this function supports currying
|
||||||
|
*/
|
||||||
|
interface IProduceWithPatches {
|
||||||
|
<Recipe extends AnyFunc>(recipe: Recipe): InferCurriedFromRecipe<Recipe, true>;
|
||||||
|
<State, Recipe extends Function>(recipe: Recipe, initialState: State): InferCurriedFromInitialStateAndRecipe<State, Recipe, true>;
|
||||||
|
<Base, D = Draft<Base>>(base: Base, recipe: (draft: D) => ValidRecipeReturnType<D>, listener?: PatchListener): PatchesTuple<Base>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The type for `recipe function`
|
||||||
|
*/
|
||||||
|
type Producer<T> = (draft: Draft<T>) => ValidRecipeReturnType<Draft<T>>;
|
||||||
|
|
||||||
|
type Objectish = AnyObject | AnyArray | AnyMap | AnySet;
|
||||||
|
type AnyObject = {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
type AnyArray = Array<any>;
|
||||||
|
type AnySet = Set<any>;
|
||||||
|
type AnyMap = Map<any, any>;
|
||||||
|
|
||||||
|
/** Returns true if the given value is an Immer draft */
|
||||||
|
declare function isDraft(value: any): boolean;
|
||||||
|
/** Returns true if the given value can be drafted by Immer */
|
||||||
|
declare function isDraftable(value: any): boolean;
|
||||||
|
/** Get the underlying object that is represented by the given draft */
|
||||||
|
declare function original<T>(value: T): T | undefined;
|
||||||
|
/**
|
||||||
|
* Freezes draftable objects. Returns the original object.
|
||||||
|
* By default freezes shallowly, but if the second argument is `true` it will freeze recursively.
|
||||||
|
*
|
||||||
|
* @param obj
|
||||||
|
* @param deep
|
||||||
|
*/
|
||||||
|
declare function freeze<T>(obj: T, deep?: boolean): T;
|
||||||
|
|
||||||
|
interface ProducersFns {
|
||||||
|
produce: IProduce;
|
||||||
|
produceWithPatches: IProduceWithPatches;
|
||||||
|
}
|
||||||
|
type StrictMode = boolean | "class_only";
|
||||||
|
declare class Immer implements ProducersFns {
|
||||||
|
autoFreeze_: boolean;
|
||||||
|
useStrictShallowCopy_: StrictMode;
|
||||||
|
constructor(config?: {
|
||||||
|
autoFreeze?: boolean;
|
||||||
|
useStrictShallowCopy?: StrictMode;
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The `produce` function takes a value and a "recipe function" (whose
|
||||||
|
* return value often depends on the base state). The recipe function is
|
||||||
|
* free to mutate its first argument however it wants. All mutations are
|
||||||
|
* only ever applied to a __copy__ of the base state.
|
||||||
|
*
|
||||||
|
* Pass only a function to create a "curried producer" which relieves you
|
||||||
|
* from passing the recipe function every time.
|
||||||
|
*
|
||||||
|
* Only plain objects and arrays are made mutable. All other objects are
|
||||||
|
* considered uncopyable.
|
||||||
|
*
|
||||||
|
* Note: This function is __bound__ to its `Immer` instance.
|
||||||
|
*
|
||||||
|
* @param {any} base - the initial state
|
||||||
|
* @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
|
||||||
|
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||||||
|
* @returns {any} a new state, or the initial state if nothing was modified
|
||||||
|
*/
|
||||||
|
produce: IProduce;
|
||||||
|
produceWithPatches: IProduceWithPatches;
|
||||||
|
createDraft<T extends Objectish>(base: T): Draft<T>;
|
||||||
|
finishDraft<D extends Draft<any>>(draft: D, patchListener?: PatchListener): D extends Draft<infer T> ? T : never;
|
||||||
|
/**
|
||||||
|
* Pass true to automatically freeze all copies created by Immer.
|
||||||
|
*
|
||||||
|
* By default, auto-freezing is enabled.
|
||||||
|
*/
|
||||||
|
setAutoFreeze(value: boolean): void;
|
||||||
|
/**
|
||||||
|
* Pass true to enable strict shallow copy.
|
||||||
|
*
|
||||||
|
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
||||||
|
*/
|
||||||
|
setUseStrictShallowCopy(value: StrictMode): void;
|
||||||
|
applyPatches<T extends Objectish>(base: T, patches: readonly Patch[]): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Takes a snapshot of the current state of a draft and finalizes it (but without freezing). This is a great utility to print the current state during debugging (no Proxies in the way). The output of current can also be safely leaked outside the producer. */
|
||||||
|
declare function current<T>(value: T): T;
|
||||||
|
|
||||||
|
declare function enablePatches(): void;
|
||||||
|
|
||||||
|
declare function enableMapSet(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `produce` function takes a value and a "recipe function" (whose
|
||||||
|
* return value often depends on the base state). The recipe function is
|
||||||
|
* free to mutate its first argument however it wants. All mutations are
|
||||||
|
* only ever applied to a __copy__ of the base state.
|
||||||
|
*
|
||||||
|
* Pass only a function to create a "curried producer" which relieves you
|
||||||
|
* from passing the recipe function every time.
|
||||||
|
*
|
||||||
|
* Only plain objects and arrays are made mutable. All other objects are
|
||||||
|
* considered uncopyable.
|
||||||
|
*
|
||||||
|
* Note: This function is __bound__ to its `Immer` instance.
|
||||||
|
*
|
||||||
|
* @param {any} base - the initial state
|
||||||
|
* @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
|
||||||
|
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||||||
|
* @returns {any} a new state, or the initial state if nothing was modified
|
||||||
|
*/
|
||||||
|
declare const produce: IProduce;
|
||||||
|
/**
|
||||||
|
* Like `produce`, but `produceWithPatches` always returns a tuple
|
||||||
|
* [nextState, patches, inversePatches] (instead of just the next state)
|
||||||
|
*/
|
||||||
|
declare const produceWithPatches: IProduceWithPatches;
|
||||||
|
/**
|
||||||
|
* Pass true to automatically freeze all copies created by Immer.
|
||||||
|
*
|
||||||
|
* Always freeze by default, even in production mode
|
||||||
|
*/
|
||||||
|
declare const setAutoFreeze: (value: boolean) => void;
|
||||||
|
/**
|
||||||
|
* Pass true to enable strict shallow copy.
|
||||||
|
*
|
||||||
|
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
||||||
|
*/
|
||||||
|
declare const setUseStrictShallowCopy: (value: StrictMode) => void;
|
||||||
|
/**
|
||||||
|
* Apply an array of Immer patches to the first argument.
|
||||||
|
*
|
||||||
|
* This function is a producer, which means copy-on-write is in effect.
|
||||||
|
*/
|
||||||
|
declare const applyPatches: <T extends Objectish>(base: T, patches: readonly Patch[]) => T;
|
||||||
|
/**
|
||||||
|
* Create an Immer draft from the given base state, which may be a draft itself.
|
||||||
|
* The draft can be modified until you finalize it with the `finishDraft` function.
|
||||||
|
*/
|
||||||
|
declare const createDraft: <T extends Objectish>(base: T) => Draft<T>;
|
||||||
|
/**
|
||||||
|
* Finalize an Immer draft from a `createDraft` call, returning the base state
|
||||||
|
* (if no changes were made) or a modified copy. The draft must *not* be
|
||||||
|
* mutated afterwards.
|
||||||
|
*
|
||||||
|
* Pass a function as the 2nd argument to generate Immer patches based on the
|
||||||
|
* changes that were made.
|
||||||
|
*/
|
||||||
|
declare const finishDraft: <D extends unknown>(draft: D, patchListener?: PatchListener | undefined) => D extends Draft<infer T> ? T : never;
|
||||||
|
/**
|
||||||
|
* This function is actually a no-op, but can be used to cast an immutable type
|
||||||
|
* to an draft type and make TypeScript happy
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
declare function castDraft<T>(value: T): Draft<T>;
|
||||||
|
/**
|
||||||
|
* This function is actually a no-op, but can be used to cast a mutable type
|
||||||
|
* to an immutable type and make TypeScript happy
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
declare function castImmutable<T>(value: T): Immutable<T>;
|
||||||
|
|
||||||
|
export { Draft, Immer, Immutable, Objectish, Patch, PatchListener, Producer, StrictMode, WritableDraft, applyPatches, castDraft, castImmutable, createDraft, current, enableMapSet, enablePatches, finishDraft, freeze, DRAFTABLE as immerable, isDraft, isDraftable, NOTHING as nothing, original, produce, produceWithPatches, setAutoFreeze, setUseStrictShallowCopy };
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,87 @@
|
||||||
|
{
|
||||||
|
"name": "immer",
|
||||||
|
"version": "10.1.1",
|
||||||
|
"description": "Create your next immutable state by mutating the current one",
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"module": "./dist/immer.legacy-esm.js",
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"types": "./dist/immer.d.ts",
|
||||||
|
"import": "./dist/immer.mjs",
|
||||||
|
"require": "./dist/cjs/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jsnext:main": "dist/immer.mjs",
|
||||||
|
"react-native": "./dist/immer.legacy-esm.js",
|
||||||
|
"source": "src/immer.ts",
|
||||||
|
"types": "./dist/immer.d.ts",
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"pretest": "yarn build",
|
||||||
|
"test": "jest && yarn test:build && yarn test:flow",
|
||||||
|
"test:perf": "cd __performance_tests__ && node add-data.mjs && node todo.mjs && node incremental.mjs && node large-obj.mjs",
|
||||||
|
"test:flow": "yarn flow check __tests__/flow",
|
||||||
|
"test:build": "NODE_ENV='production' yarn jest --config jest.config.build.js",
|
||||||
|
"watch": "jest --watch",
|
||||||
|
"coverage": "jest --coverage",
|
||||||
|
"coveralls": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf ./coverage",
|
||||||
|
"build": "tsup",
|
||||||
|
"publish-docs": "cd website && GIT_USER=mweststrate USE_SSH=true yarn docusaurus deploy",
|
||||||
|
"start": "cd website && yarn start",
|
||||||
|
"test:size": "yarn build && yarn import-size --report . produce enableMapSet enablePatches",
|
||||||
|
"test:sizequick": "yarn build && yarn import-size . produce"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "pretty-quick --staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/immerjs/immer.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"immutable",
|
||||||
|
"mutable",
|
||||||
|
"copy-on-write"
|
||||||
|
],
|
||||||
|
"author": "Michel Weststrate <info@michel.codes>",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/immerjs/immer/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/immerjs/immer#readme",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"compat",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.21.3",
|
||||||
|
"@types/jest": "^25.1.2",
|
||||||
|
"coveralls": "^3.0.0",
|
||||||
|
"cpx2": "^3.0.0",
|
||||||
|
"deep-freeze": "^0.0.1",
|
||||||
|
"flow-bin": "^0.123.0",
|
||||||
|
"husky": "^1.2.0",
|
||||||
|
"immutable": "^3.8.2",
|
||||||
|
"import-size": "^1.0.2",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"lodash": "^4.17.4",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"prettier": "1.19.1",
|
||||||
|
"pretty-quick": "^1.8.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
|
"seamless-immutable": "^7.1.3",
|
||||||
|
"semantic-release": "^17.0.2",
|
||||||
|
"ts-jest": "^29.0.0",
|
||||||
|
"tsup": "^6.7.0",
|
||||||
|
"typescript": "^5.0.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<img src="images/immer-logo.svg" height="200px" align="right"/>
|
||||||
|
|
||||||
|
# Immer
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/immer) [](https://github.com/immerjs/immer/actions?query=branch%3Amain) [](https://coveralls.io/github/immerjs/immer?branch=main) [](https://github.com/prettier/prettier) [](#backers) [](#sponsors) [](https://gitpod.io/#https://github.com/immerjs/immer)
|
||||||
|
|
||||||
|
_Create the next immutable state tree by simply modifying the current tree_
|
||||||
|
|
||||||
|
Winner of the "Breakthrough of the year" [React open source award](https://osawards.com/react/) and "Most impactful contribution" [JavaScript open source award](https://osawards.com/javascript/) in 2019
|
||||||
|
|
||||||
|
## Contribute using one-click online setup
|
||||||
|
|
||||||
|
You can use Gitpod (a free online VS Code like IDE) for contributing online. With a single click it will launch a workspace and automatically:
|
||||||
|
|
||||||
|
- clone the immer repo.
|
||||||
|
- install the dependencies.
|
||||||
|
- run `yarn run start`.
|
||||||
|
|
||||||
|
so that you can start coding straight away.
|
||||||
|
|
||||||
|
[](https://gitpod.io/from-referrer/)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
The documentation of this package is hosted at https://immerjs.github.io/immer/
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Did Immer make a difference to your project? Join the open collective at https://opencollective.com/immer!
|
||||||
|
|
||||||
|
## Release notes
|
||||||
|
|
||||||
|
https://github.com/immerjs/immer/releases
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
die,
|
||||||
|
isDraft,
|
||||||
|
shallowCopy,
|
||||||
|
each,
|
||||||
|
DRAFT_STATE,
|
||||||
|
set,
|
||||||
|
ImmerState,
|
||||||
|
isDraftable,
|
||||||
|
isFrozen
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
/** Takes a snapshot of the current state of a draft and finalizes it (but without freezing). This is a great utility to print the current state during debugging (no Proxies in the way). The output of current can also be safely leaked outside the producer. */
|
||||||
|
export function current<T>(value: T): T
|
||||||
|
export function current(value: any): any {
|
||||||
|
if (!isDraft(value)) die(10, value)
|
||||||
|
return currentImpl(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentImpl(value: any): any {
|
||||||
|
if (!isDraftable(value) || isFrozen(value)) return value
|
||||||
|
const state: ImmerState | undefined = value[DRAFT_STATE]
|
||||||
|
let copy: any
|
||||||
|
if (state) {
|
||||||
|
if (!state.modified_) return state.base_
|
||||||
|
// Optimization: avoid generating new drafts during copying
|
||||||
|
state.finalized_ = true
|
||||||
|
copy = shallowCopy(value, state.scope_.immer_.useStrictShallowCopy_)
|
||||||
|
} else {
|
||||||
|
copy = shallowCopy(value, true)
|
||||||
|
}
|
||||||
|
// recurse
|
||||||
|
each(copy, (key, childValue) => {
|
||||||
|
set(copy, key, currentImpl(childValue))
|
||||||
|
})
|
||||||
|
if (state) {
|
||||||
|
state.finalized_ = false
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
import {
|
||||||
|
ImmerScope,
|
||||||
|
DRAFT_STATE,
|
||||||
|
isDraftable,
|
||||||
|
NOTHING,
|
||||||
|
PatchPath,
|
||||||
|
each,
|
||||||
|
has,
|
||||||
|
freeze,
|
||||||
|
ImmerState,
|
||||||
|
isDraft,
|
||||||
|
SetState,
|
||||||
|
set,
|
||||||
|
ArchType,
|
||||||
|
getPlugin,
|
||||||
|
die,
|
||||||
|
revokeScope,
|
||||||
|
isFrozen
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
export function processResult(result: any, scope: ImmerScope) {
|
||||||
|
scope.unfinalizedDrafts_ = scope.drafts_.length
|
||||||
|
const baseDraft = scope.drafts_![0]
|
||||||
|
const isReplaced = result !== undefined && result !== baseDraft
|
||||||
|
if (isReplaced) {
|
||||||
|
if (baseDraft[DRAFT_STATE].modified_) {
|
||||||
|
revokeScope(scope)
|
||||||
|
die(4)
|
||||||
|
}
|
||||||
|
if (isDraftable(result)) {
|
||||||
|
// Finalize the result in case it contains (or is) a subset of the draft.
|
||||||
|
result = finalize(scope, result)
|
||||||
|
if (!scope.parent_) maybeFreeze(scope, result)
|
||||||
|
}
|
||||||
|
if (scope.patches_) {
|
||||||
|
getPlugin("Patches").generateReplacementPatches_(
|
||||||
|
baseDraft[DRAFT_STATE].base_,
|
||||||
|
result,
|
||||||
|
scope.patches_,
|
||||||
|
scope.inversePatches_!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Finalize the base draft.
|
||||||
|
result = finalize(scope, baseDraft, [])
|
||||||
|
}
|
||||||
|
revokeScope(scope)
|
||||||
|
if (scope.patches_) {
|
||||||
|
scope.patchListener_!(scope.patches_, scope.inversePatches_!)
|
||||||
|
}
|
||||||
|
return result !== NOTHING ? result : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) {
|
||||||
|
// Don't recurse in tho recursive data structures
|
||||||
|
if (isFrozen(value)) return value
|
||||||
|
|
||||||
|
const state: ImmerState = value[DRAFT_STATE]
|
||||||
|
// A plain object, might need freezing, might contain drafts
|
||||||
|
if (!state) {
|
||||||
|
each(value, (key, childValue) =>
|
||||||
|
finalizeProperty(rootScope, state, value, key, childValue, path)
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
// Never finalize drafts owned by another scope.
|
||||||
|
if (state.scope_ !== rootScope) return value
|
||||||
|
// Unmodified draft, return the (frozen) original
|
||||||
|
if (!state.modified_) {
|
||||||
|
maybeFreeze(rootScope, state.base_, true)
|
||||||
|
return state.base_
|
||||||
|
}
|
||||||
|
// Not finalized yet, let's do that now
|
||||||
|
if (!state.finalized_) {
|
||||||
|
state.finalized_ = true
|
||||||
|
state.scope_.unfinalizedDrafts_--
|
||||||
|
const result = state.copy_
|
||||||
|
// Finalize all children of the copy
|
||||||
|
// For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
|
||||||
|
// To preserve insertion order in all cases we then clear the set
|
||||||
|
// And we let finalizeProperty know it needs to re-add non-draft children back to the target
|
||||||
|
let resultEach = result
|
||||||
|
let isSet = false
|
||||||
|
if (state.type_ === ArchType.Set) {
|
||||||
|
resultEach = new Set(result)
|
||||||
|
result.clear()
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
each(resultEach, (key, childValue) =>
|
||||||
|
finalizeProperty(rootScope, state, result, key, childValue, path, isSet)
|
||||||
|
)
|
||||||
|
// everything inside is frozen, we can freeze here
|
||||||
|
maybeFreeze(rootScope, result, false)
|
||||||
|
// first time finalizing, let's create those patches
|
||||||
|
if (path && rootScope.patches_) {
|
||||||
|
getPlugin("Patches").generatePatches_(
|
||||||
|
state,
|
||||||
|
path,
|
||||||
|
rootScope.patches_,
|
||||||
|
rootScope.inversePatches_!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.copy_
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeProperty(
|
||||||
|
rootScope: ImmerScope,
|
||||||
|
parentState: undefined | ImmerState,
|
||||||
|
targetObject: any,
|
||||||
|
prop: string | number,
|
||||||
|
childValue: any,
|
||||||
|
rootPath?: PatchPath,
|
||||||
|
targetIsSet?: boolean
|
||||||
|
) {
|
||||||
|
if (process.env.NODE_ENV !== "production" && childValue === targetObject)
|
||||||
|
die(5)
|
||||||
|
if (isDraft(childValue)) {
|
||||||
|
const path =
|
||||||
|
rootPath &&
|
||||||
|
parentState &&
|
||||||
|
parentState!.type_ !== ArchType.Set && // Set objects are atomic since they have no keys.
|
||||||
|
!has((parentState as Exclude<ImmerState, SetState>).assigned_!, prop) // Skip deep patches for assigned keys.
|
||||||
|
? rootPath!.concat(prop)
|
||||||
|
: undefined
|
||||||
|
// Drafts owned by `scope` are finalized here.
|
||||||
|
const res = finalize(rootScope, childValue, path)
|
||||||
|
set(targetObject, prop, res)
|
||||||
|
// Drafts from another scope must prevented to be frozen
|
||||||
|
// if we got a draft back from finalize, we're in a nested produce and shouldn't freeze
|
||||||
|
if (isDraft(res)) {
|
||||||
|
rootScope.canAutoFreeze_ = false
|
||||||
|
} else return
|
||||||
|
} else if (targetIsSet) {
|
||||||
|
targetObject.add(childValue)
|
||||||
|
}
|
||||||
|
// Search new objects for unfinalized drafts. Frozen objects should never contain drafts.
|
||||||
|
if (isDraftable(childValue) && !isFrozen(childValue)) {
|
||||||
|
if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) {
|
||||||
|
// optimization: if an object is not a draft, and we don't have to
|
||||||
|
// deepfreeze everything, and we are sure that no drafts are left in the remaining object
|
||||||
|
// cause we saw and finalized all drafts already; we can stop visiting the rest of the tree.
|
||||||
|
// This benefits especially adding large data tree's without further processing.
|
||||||
|
// See add-data.js perf test
|
||||||
|
return
|
||||||
|
}
|
||||||
|
finalize(rootScope, childValue)
|
||||||
|
// Immer deep freezes plain objects, so if there is no parent state, we freeze as well
|
||||||
|
// Per #590, we never freeze symbolic properties. Just to make sure don't accidentally interfere
|
||||||
|
// with other frameworks.
|
||||||
|
if (
|
||||||
|
(!parentState || !parentState.scope_.parent_) &&
|
||||||
|
typeof prop !== "symbol" &&
|
||||||
|
Object.prototype.propertyIsEnumerable.call(targetObject, prop)
|
||||||
|
)
|
||||||
|
maybeFreeze(rootScope, childValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeFreeze(scope: ImmerScope, value: any, deep = false) {
|
||||||
|
// we never freeze for a non-root scope; as it would prevent pruning for drafts inside wrapping objects
|
||||||
|
if (!scope.parent_ && scope.immer_.autoFreeze_ && scope.canAutoFreeze_) {
|
||||||
|
freeze(value, deep)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
import {
|
||||||
|
IProduceWithPatches,
|
||||||
|
IProduce,
|
||||||
|
ImmerState,
|
||||||
|
Drafted,
|
||||||
|
isDraftable,
|
||||||
|
processResult,
|
||||||
|
Patch,
|
||||||
|
Objectish,
|
||||||
|
DRAFT_STATE,
|
||||||
|
Draft,
|
||||||
|
PatchListener,
|
||||||
|
isDraft,
|
||||||
|
isMap,
|
||||||
|
isSet,
|
||||||
|
createProxyProxy,
|
||||||
|
getPlugin,
|
||||||
|
die,
|
||||||
|
enterScope,
|
||||||
|
revokeScope,
|
||||||
|
leaveScope,
|
||||||
|
usePatchesInScope,
|
||||||
|
getCurrentScope,
|
||||||
|
NOTHING,
|
||||||
|
freeze,
|
||||||
|
current
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
interface ProducersFns {
|
||||||
|
produce: IProduce
|
||||||
|
produceWithPatches: IProduceWithPatches
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StrictMode = boolean | "class_only";
|
||||||
|
|
||||||
|
export class Immer implements ProducersFns {
|
||||||
|
autoFreeze_: boolean = true
|
||||||
|
useStrictShallowCopy_: StrictMode = false
|
||||||
|
|
||||||
|
constructor(config?: {
|
||||||
|
autoFreeze?: boolean
|
||||||
|
useStrictShallowCopy?: StrictMode
|
||||||
|
}) {
|
||||||
|
if (typeof config?.autoFreeze === "boolean")
|
||||||
|
this.setAutoFreeze(config!.autoFreeze)
|
||||||
|
if (typeof config?.useStrictShallowCopy === "boolean")
|
||||||
|
this.setUseStrictShallowCopy(config!.useStrictShallowCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `produce` function takes a value and a "recipe function" (whose
|
||||||
|
* return value often depends on the base state). The recipe function is
|
||||||
|
* free to mutate its first argument however it wants. All mutations are
|
||||||
|
* only ever applied to a __copy__ of the base state.
|
||||||
|
*
|
||||||
|
* Pass only a function to create a "curried producer" which relieves you
|
||||||
|
* from passing the recipe function every time.
|
||||||
|
*
|
||||||
|
* Only plain objects and arrays are made mutable. All other objects are
|
||||||
|
* considered uncopyable.
|
||||||
|
*
|
||||||
|
* Note: This function is __bound__ to its `Immer` instance.
|
||||||
|
*
|
||||||
|
* @param {any} base - the initial state
|
||||||
|
* @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
|
||||||
|
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||||||
|
* @returns {any} a new state, or the initial state if nothing was modified
|
||||||
|
*/
|
||||||
|
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
|
||||||
|
// curried invocation
|
||||||
|
if (typeof base === "function" && typeof recipe !== "function") {
|
||||||
|
const defaultBase = recipe
|
||||||
|
recipe = base
|
||||||
|
|
||||||
|
const self = this
|
||||||
|
return function curriedProduce(
|
||||||
|
this: any,
|
||||||
|
base = defaultBase,
|
||||||
|
...args: any[]
|
||||||
|
) {
|
||||||
|
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof recipe !== "function") die(6)
|
||||||
|
if (patchListener !== undefined && typeof patchListener !== "function")
|
||||||
|
die(7)
|
||||||
|
|
||||||
|
let result
|
||||||
|
|
||||||
|
// Only plain objects, arrays, and "immerable classes" are drafted.
|
||||||
|
if (isDraftable(base)) {
|
||||||
|
const scope = enterScope(this)
|
||||||
|
const proxy = createProxy(base, undefined)
|
||||||
|
let hasError = true
|
||||||
|
try {
|
||||||
|
result = recipe(proxy)
|
||||||
|
hasError = false
|
||||||
|
} finally {
|
||||||
|
// finally instead of catch + rethrow better preserves original stack
|
||||||
|
if (hasError) revokeScope(scope)
|
||||||
|
else leaveScope(scope)
|
||||||
|
}
|
||||||
|
usePatchesInScope(scope, patchListener)
|
||||||
|
return processResult(result, scope)
|
||||||
|
} else if (!base || typeof base !== "object") {
|
||||||
|
result = recipe(base)
|
||||||
|
if (result === undefined) result = base
|
||||||
|
if (result === NOTHING) result = undefined
|
||||||
|
if (this.autoFreeze_) freeze(result, true)
|
||||||
|
if (patchListener) {
|
||||||
|
const p: Patch[] = []
|
||||||
|
const ip: Patch[] = []
|
||||||
|
getPlugin("Patches").generateReplacementPatches_(base, result, p, ip)
|
||||||
|
patchListener(p, ip)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} else die(1, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
produceWithPatches: IProduceWithPatches = (base: any, recipe?: any): any => {
|
||||||
|
// curried invocation
|
||||||
|
if (typeof base === "function") {
|
||||||
|
return (state: any, ...args: any[]) =>
|
||||||
|
this.produceWithPatches(state, (draft: any) => base(draft, ...args))
|
||||||
|
}
|
||||||
|
|
||||||
|
let patches: Patch[], inversePatches: Patch[]
|
||||||
|
const result = this.produce(base, recipe, (p: Patch[], ip: Patch[]) => {
|
||||||
|
patches = p
|
||||||
|
inversePatches = ip
|
||||||
|
})
|
||||||
|
return [result, patches!, inversePatches!]
|
||||||
|
}
|
||||||
|
|
||||||
|
createDraft<T extends Objectish>(base: T): Draft<T> {
|
||||||
|
if (!isDraftable(base)) die(8)
|
||||||
|
if (isDraft(base)) base = current(base)
|
||||||
|
const scope = enterScope(this)
|
||||||
|
const proxy = createProxy(base, undefined)
|
||||||
|
proxy[DRAFT_STATE].isManual_ = true
|
||||||
|
leaveScope(scope)
|
||||||
|
return proxy as any
|
||||||
|
}
|
||||||
|
|
||||||
|
finishDraft<D extends Draft<any>>(
|
||||||
|
draft: D,
|
||||||
|
patchListener?: PatchListener
|
||||||
|
): D extends Draft<infer T> ? T : never {
|
||||||
|
const state: ImmerState = draft && (draft as any)[DRAFT_STATE]
|
||||||
|
if (!state || !state.isManual_) die(9)
|
||||||
|
const {scope_: scope} = state
|
||||||
|
usePatchesInScope(scope, patchListener)
|
||||||
|
return processResult(undefined, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass true to automatically freeze all copies created by Immer.
|
||||||
|
*
|
||||||
|
* By default, auto-freezing is enabled.
|
||||||
|
*/
|
||||||
|
setAutoFreeze(value: boolean) {
|
||||||
|
this.autoFreeze_ = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass true to enable strict shallow copy.
|
||||||
|
*
|
||||||
|
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
||||||
|
*/
|
||||||
|
setUseStrictShallowCopy(value: StrictMode) {
|
||||||
|
this.useStrictShallowCopy_ = value
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPatches<T extends Objectish>(base: T, patches: readonly Patch[]): T {
|
||||||
|
// If a patch replaces the entire state, take that replacement as base
|
||||||
|
// before applying patches
|
||||||
|
let i: number
|
||||||
|
for (i = patches.length - 1; i >= 0; i--) {
|
||||||
|
const patch = patches[i]
|
||||||
|
if (patch.path.length === 0 && patch.op === "replace") {
|
||||||
|
base = patch.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there was a patch that replaced the entire state, start from the
|
||||||
|
// patch after that.
|
||||||
|
if (i > -1) {
|
||||||
|
patches = patches.slice(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyPatchesImpl = getPlugin("Patches").applyPatches_
|
||||||
|
if (isDraft(base)) {
|
||||||
|
// N.B: never hits if some patch a replacement, patches are never drafts
|
||||||
|
return applyPatchesImpl(base, patches)
|
||||||
|
}
|
||||||
|
// Otherwise, produce a copy of the base state.
|
||||||
|
return this.produce(base, (draft: Drafted) =>
|
||||||
|
applyPatchesImpl(draft, patches)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProxy<T extends Objectish>(
|
||||||
|
value: T,
|
||||||
|
parent?: ImmerState
|
||||||
|
): Drafted<T, ImmerState> {
|
||||||
|
// precondition: createProxy should be guarded by isDraftable, so we know we can safely draft
|
||||||
|
const draft: Drafted = isMap(value)
|
||||||
|
? getPlugin("MapSet").proxyMap_(value, parent)
|
||||||
|
: isSet(value)
|
||||||
|
? getPlugin("MapSet").proxySet_(value, parent)
|
||||||
|
: createProxyProxy(value, parent)
|
||||||
|
|
||||||
|
const scope = parent ? parent.scope_ : getCurrentScope()
|
||||||
|
scope.drafts_.push(draft)
|
||||||
|
return draft
|
||||||
|
}
|
|
@ -0,0 +1,292 @@
|
||||||
|
import {
|
||||||
|
each,
|
||||||
|
has,
|
||||||
|
is,
|
||||||
|
isDraftable,
|
||||||
|
shallowCopy,
|
||||||
|
latest,
|
||||||
|
ImmerBaseState,
|
||||||
|
ImmerState,
|
||||||
|
Drafted,
|
||||||
|
AnyObject,
|
||||||
|
AnyArray,
|
||||||
|
Objectish,
|
||||||
|
getCurrentScope,
|
||||||
|
getPrototypeOf,
|
||||||
|
DRAFT_STATE,
|
||||||
|
die,
|
||||||
|
createProxy,
|
||||||
|
ArchType,
|
||||||
|
ImmerScope
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
interface ProxyBaseState extends ImmerBaseState {
|
||||||
|
assigned_: {
|
||||||
|
[property: string]: boolean
|
||||||
|
}
|
||||||
|
parent_?: ImmerState
|
||||||
|
revoke_(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProxyObjectState extends ProxyBaseState {
|
||||||
|
type_: ArchType.Object
|
||||||
|
base_: any
|
||||||
|
copy_: any
|
||||||
|
draft_: Drafted<AnyObject, ProxyObjectState>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProxyArrayState extends ProxyBaseState {
|
||||||
|
type_: ArchType.Array
|
||||||
|
base_: AnyArray
|
||||||
|
copy_: AnyArray | null
|
||||||
|
draft_: Drafted<AnyArray, ProxyArrayState>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyState = ProxyObjectState | ProxyArrayState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new draft of the `base` object.
|
||||||
|
*
|
||||||
|
* The second argument is the parent draft-state (used internally).
|
||||||
|
*/
|
||||||
|
export function createProxyProxy<T extends Objectish>(
|
||||||
|
base: T,
|
||||||
|
parent?: ImmerState
|
||||||
|
): Drafted<T, ProxyState> {
|
||||||
|
const isArray = Array.isArray(base)
|
||||||
|
const state: ProxyState = {
|
||||||
|
type_: isArray ? ArchType.Array : (ArchType.Object as any),
|
||||||
|
// Track which produce call this is associated with.
|
||||||
|
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||||||
|
// True for both shallow and deep changes.
|
||||||
|
modified_: false,
|
||||||
|
// Used during finalization.
|
||||||
|
finalized_: false,
|
||||||
|
// Track which properties have been assigned (true) or deleted (false).
|
||||||
|
assigned_: {},
|
||||||
|
// The parent draft state.
|
||||||
|
parent_: parent,
|
||||||
|
// The base state.
|
||||||
|
base_: base,
|
||||||
|
// The base proxy.
|
||||||
|
draft_: null as any, // set below
|
||||||
|
// The base copy with any updated values.
|
||||||
|
copy_: null,
|
||||||
|
// Called by the `produce` function.
|
||||||
|
revoke_: null as any,
|
||||||
|
isManual_: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// the traps must target something, a bit like the 'real' base.
|
||||||
|
// but also, we need to be able to determine from the target what the relevant state is
|
||||||
|
// (to avoid creating traps per instance to capture the state in closure,
|
||||||
|
// and to avoid creating weird hidden properties as well)
|
||||||
|
// So the trick is to use 'state' as the actual 'target'! (and make sure we intercept everything)
|
||||||
|
// Note that in the case of an array, we put the state in an array to have better Reflect defaults ootb
|
||||||
|
let target: T = state as any
|
||||||
|
let traps: ProxyHandler<object | Array<any>> = objectTraps
|
||||||
|
if (isArray) {
|
||||||
|
target = [state] as any
|
||||||
|
traps = arrayTraps
|
||||||
|
}
|
||||||
|
|
||||||
|
const {revoke, proxy} = Proxy.revocable(target, traps)
|
||||||
|
state.draft_ = proxy as any
|
||||||
|
state.revoke_ = revoke
|
||||||
|
return proxy as any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object drafts
|
||||||
|
*/
|
||||||
|
export const objectTraps: ProxyHandler<ProxyState> = {
|
||||||
|
get(state, prop) {
|
||||||
|
if (prop === DRAFT_STATE) return state
|
||||||
|
|
||||||
|
const source = latest(state)
|
||||||
|
if (!has(source, prop)) {
|
||||||
|
// non-existing or non-own property...
|
||||||
|
return readPropFromProto(state, source, prop)
|
||||||
|
}
|
||||||
|
const value = source[prop]
|
||||||
|
if (state.finalized_ || !isDraftable(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
// Check for existing draft in modified state.
|
||||||
|
// Assigned values are never drafted. This catches any drafts we created, too.
|
||||||
|
if (value === peek(state.base_, prop)) {
|
||||||
|
prepareCopy(state)
|
||||||
|
return (state.copy_![prop as any] = createProxy(value, state))
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
has(state, prop) {
|
||||||
|
return prop in latest(state)
|
||||||
|
},
|
||||||
|
ownKeys(state) {
|
||||||
|
return Reflect.ownKeys(latest(state))
|
||||||
|
},
|
||||||
|
set(
|
||||||
|
state: ProxyObjectState,
|
||||||
|
prop: string /* strictly not, but helps TS */,
|
||||||
|
value
|
||||||
|
) {
|
||||||
|
const desc = getDescriptorFromProto(latest(state), prop)
|
||||||
|
if (desc?.set) {
|
||||||
|
// special case: if this write is captured by a setter, we have
|
||||||
|
// to trigger it with the correct context
|
||||||
|
desc.set.call(state.draft_, value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!state.modified_) {
|
||||||
|
// the last check is because we need to be able to distinguish setting a non-existing to undefined (which is a change)
|
||||||
|
// from setting an existing property with value undefined to undefined (which is not a change)
|
||||||
|
const current = peek(latest(state), prop)
|
||||||
|
// special case, if we assigning the original value to a draft, we can ignore the assignment
|
||||||
|
const currentState: ProxyObjectState = current?.[DRAFT_STATE]
|
||||||
|
if (currentState && currentState.base_ === value) {
|
||||||
|
state.copy_![prop] = value
|
||||||
|
state.assigned_[prop] = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (is(value, current) && (value !== undefined || has(state.base_, prop)))
|
||||||
|
return true
|
||||||
|
prepareCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(state.copy_![prop] === value &&
|
||||||
|
// special case: handle new props with value 'undefined'
|
||||||
|
(value !== undefined || prop in state.copy_)) ||
|
||||||
|
// special case: NaN
|
||||||
|
(Number.isNaN(value) && Number.isNaN(state.copy_![prop]))
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
state.copy_![prop] = value
|
||||||
|
state.assigned_[prop] = true
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
deleteProperty(state, prop: string) {
|
||||||
|
// The `undefined` check is a fast path for pre-existing keys.
|
||||||
|
if (peek(state.base_, prop) !== undefined || prop in state.base_) {
|
||||||
|
state.assigned_[prop] = false
|
||||||
|
prepareCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
} else {
|
||||||
|
// if an originally not assigned property was deleted
|
||||||
|
delete state.assigned_[prop]
|
||||||
|
}
|
||||||
|
if (state.copy_) {
|
||||||
|
delete state.copy_[prop]
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
// Note: We never coerce `desc.value` into an Immer draft, because we can't make
|
||||||
|
// the same guarantee in ES5 mode.
|
||||||
|
getOwnPropertyDescriptor(state, prop) {
|
||||||
|
const owner = latest(state)
|
||||||
|
const desc = Reflect.getOwnPropertyDescriptor(owner, prop)
|
||||||
|
if (!desc) return desc
|
||||||
|
return {
|
||||||
|
writable: true,
|
||||||
|
configurable: state.type_ !== ArchType.Array || prop !== "length",
|
||||||
|
enumerable: desc.enumerable,
|
||||||
|
value: owner[prop]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defineProperty() {
|
||||||
|
die(11)
|
||||||
|
},
|
||||||
|
getPrototypeOf(state) {
|
||||||
|
return getPrototypeOf(state.base_)
|
||||||
|
},
|
||||||
|
setPrototypeOf() {
|
||||||
|
die(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array drafts
|
||||||
|
*/
|
||||||
|
|
||||||
|
const arrayTraps: ProxyHandler<[ProxyArrayState]> = {}
|
||||||
|
each(objectTraps, (key, fn) => {
|
||||||
|
// @ts-ignore
|
||||||
|
arrayTraps[key] = function() {
|
||||||
|
arguments[0] = arguments[0][0]
|
||||||
|
return fn.apply(this, arguments)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
arrayTraps.deleteProperty = function(state, prop) {
|
||||||
|
if (process.env.NODE_ENV !== "production" && isNaN(parseInt(prop as any)))
|
||||||
|
die(13)
|
||||||
|
// @ts-ignore
|
||||||
|
return arrayTraps.set!.call(this, state, prop, undefined)
|
||||||
|
}
|
||||||
|
arrayTraps.set = function(state, prop, value) {
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV !== "production" &&
|
||||||
|
prop !== "length" &&
|
||||||
|
isNaN(parseInt(prop as any))
|
||||||
|
)
|
||||||
|
die(14)
|
||||||
|
return objectTraps.set!.call(this, state[0], prop, value, state[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access a property without creating an Immer draft.
|
||||||
|
function peek(draft: Drafted, prop: PropertyKey) {
|
||||||
|
const state = draft[DRAFT_STATE]
|
||||||
|
const source = state ? latest(state) : draft
|
||||||
|
return source[prop]
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPropFromProto(state: ImmerState, source: any, prop: PropertyKey) {
|
||||||
|
const desc = getDescriptorFromProto(source, prop)
|
||||||
|
return desc
|
||||||
|
? `value` in desc
|
||||||
|
? desc.value
|
||||||
|
: // This is a very special case, if the prop is a getter defined by the
|
||||||
|
// prototype, we should invoke it with the draft as context!
|
||||||
|
desc.get?.call(state.draft_)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDescriptorFromProto(
|
||||||
|
source: any,
|
||||||
|
prop: PropertyKey
|
||||||
|
): PropertyDescriptor | undefined {
|
||||||
|
// 'in' checks proto!
|
||||||
|
if (!(prop in source)) return undefined
|
||||||
|
let proto = getPrototypeOf(source)
|
||||||
|
while (proto) {
|
||||||
|
const desc = Object.getOwnPropertyDescriptor(proto, prop)
|
||||||
|
if (desc) return desc
|
||||||
|
proto = getPrototypeOf(proto)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markChanged(state: ImmerState) {
|
||||||
|
if (!state.modified_) {
|
||||||
|
state.modified_ = true
|
||||||
|
if (state.parent_) {
|
||||||
|
markChanged(state.parent_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareCopy(state: {
|
||||||
|
base_: any
|
||||||
|
copy_: any
|
||||||
|
scope_: ImmerScope
|
||||||
|
}) {
|
||||||
|
if (!state.copy_) {
|
||||||
|
state.copy_ = shallowCopy(
|
||||||
|
state.base_,
|
||||||
|
state.scope_.immer_.useStrictShallowCopy_
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {
|
||||||
|
Patch,
|
||||||
|
PatchListener,
|
||||||
|
Drafted,
|
||||||
|
Immer,
|
||||||
|
DRAFT_STATE,
|
||||||
|
ImmerState,
|
||||||
|
ArchType,
|
||||||
|
getPlugin
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
/** Each scope represents a `produce` call. */
|
||||||
|
|
||||||
|
export interface ImmerScope {
|
||||||
|
patches_?: Patch[]
|
||||||
|
inversePatches_?: Patch[]
|
||||||
|
canAutoFreeze_: boolean
|
||||||
|
drafts_: any[]
|
||||||
|
parent_?: ImmerScope
|
||||||
|
patchListener_?: PatchListener
|
||||||
|
immer_: Immer
|
||||||
|
unfinalizedDrafts_: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentScope: ImmerScope | undefined
|
||||||
|
|
||||||
|
export function getCurrentScope() {
|
||||||
|
return currentScope!
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScope(
|
||||||
|
parent_: ImmerScope | undefined,
|
||||||
|
immer_: Immer
|
||||||
|
): ImmerScope {
|
||||||
|
return {
|
||||||
|
drafts_: [],
|
||||||
|
parent_,
|
||||||
|
immer_,
|
||||||
|
// Whenever the modified draft contains a draft from another scope, we
|
||||||
|
// need to prevent auto-freezing so the unowned draft can be finalized.
|
||||||
|
canAutoFreeze_: true,
|
||||||
|
unfinalizedDrafts_: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePatchesInScope(
|
||||||
|
scope: ImmerScope,
|
||||||
|
patchListener?: PatchListener
|
||||||
|
) {
|
||||||
|
if (patchListener) {
|
||||||
|
getPlugin("Patches") // assert we have the plugin
|
||||||
|
scope.patches_ = []
|
||||||
|
scope.inversePatches_ = []
|
||||||
|
scope.patchListener_ = patchListener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revokeScope(scope: ImmerScope) {
|
||||||
|
leaveScope(scope)
|
||||||
|
scope.drafts_.forEach(revokeDraft)
|
||||||
|
// @ts-ignore
|
||||||
|
scope.drafts_ = null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function leaveScope(scope: ImmerScope) {
|
||||||
|
if (scope === currentScope) {
|
||||||
|
currentScope = scope.parent_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enterScope(immer: Immer) {
|
||||||
|
return (currentScope = createScope(currentScope, immer))
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeDraft(draft: Drafted) {
|
||||||
|
const state: ImmerState = draft[DRAFT_STATE]
|
||||||
|
if (state.type_ === ArchType.Object || state.type_ === ArchType.Array)
|
||||||
|
state.revoke_()
|
||||||
|
else state.revoked_ = true
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
import {
|
||||||
|
IProduce,
|
||||||
|
IProduceWithPatches,
|
||||||
|
Immer,
|
||||||
|
Draft,
|
||||||
|
Immutable
|
||||||
|
} from "./internal"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Draft,
|
||||||
|
WritableDraft,
|
||||||
|
Immutable,
|
||||||
|
Patch,
|
||||||
|
PatchListener,
|
||||||
|
Producer,
|
||||||
|
original,
|
||||||
|
current,
|
||||||
|
isDraft,
|
||||||
|
isDraftable,
|
||||||
|
NOTHING as nothing,
|
||||||
|
DRAFTABLE as immerable,
|
||||||
|
freeze,
|
||||||
|
Objectish,
|
||||||
|
StrictMode
|
||||||
|
} from "./internal"
|
||||||
|
|
||||||
|
const immer = new Immer()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `produce` function takes a value and a "recipe function" (whose
|
||||||
|
* return value often depends on the base state). The recipe function is
|
||||||
|
* free to mutate its first argument however it wants. All mutations are
|
||||||
|
* only ever applied to a __copy__ of the base state.
|
||||||
|
*
|
||||||
|
* Pass only a function to create a "curried producer" which relieves you
|
||||||
|
* from passing the recipe function every time.
|
||||||
|
*
|
||||||
|
* Only plain objects and arrays are made mutable. All other objects are
|
||||||
|
* considered uncopyable.
|
||||||
|
*
|
||||||
|
* Note: This function is __bound__ to its `Immer` instance.
|
||||||
|
*
|
||||||
|
* @param {any} base - the initial state
|
||||||
|
* @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
|
||||||
|
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||||||
|
* @returns {any} a new state, or the initial state if nothing was modified
|
||||||
|
*/
|
||||||
|
export const produce: IProduce = immer.produce
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `produce`, but `produceWithPatches` always returns a tuple
|
||||||
|
* [nextState, patches, inversePatches] (instead of just the next state)
|
||||||
|
*/
|
||||||
|
export const produceWithPatches: IProduceWithPatches = immer.produceWithPatches.bind(
|
||||||
|
immer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass true to automatically freeze all copies created by Immer.
|
||||||
|
*
|
||||||
|
* Always freeze by default, even in production mode
|
||||||
|
*/
|
||||||
|
export const setAutoFreeze = immer.setAutoFreeze.bind(immer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass true to enable strict shallow copy.
|
||||||
|
*
|
||||||
|
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
||||||
|
*/
|
||||||
|
export const setUseStrictShallowCopy = immer.setUseStrictShallowCopy.bind(immer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an array of Immer patches to the first argument.
|
||||||
|
*
|
||||||
|
* This function is a producer, which means copy-on-write is in effect.
|
||||||
|
*/
|
||||||
|
export const applyPatches = immer.applyPatches.bind(immer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an Immer draft from the given base state, which may be a draft itself.
|
||||||
|
* The draft can be modified until you finalize it with the `finishDraft` function.
|
||||||
|
*/
|
||||||
|
export const createDraft = immer.createDraft.bind(immer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalize an Immer draft from a `createDraft` call, returning the base state
|
||||||
|
* (if no changes were made) or a modified copy. The draft must *not* be
|
||||||
|
* mutated afterwards.
|
||||||
|
*
|
||||||
|
* Pass a function as the 2nd argument to generate Immer patches based on the
|
||||||
|
* changes that were made.
|
||||||
|
*/
|
||||||
|
export const finishDraft = immer.finishDraft.bind(immer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is actually a no-op, but can be used to cast an immutable type
|
||||||
|
* to an draft type and make TypeScript happy
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function castDraft<T>(value: T): Draft<T> {
|
||||||
|
return value as any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is actually a no-op, but can be used to cast a mutable type
|
||||||
|
* to an immutable type and make TypeScript happy
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function castImmutable<T>(value: T): Immutable<T> {
|
||||||
|
return value as any
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Immer}
|
||||||
|
|
||||||
|
export {enablePatches} from "./plugins/patches"
|
||||||
|
export {enableMapSet} from "./plugins/mapset"
|
|
@ -0,0 +1,11 @@
|
||||||
|
export * from "./utils/env"
|
||||||
|
export * from "./utils/errors"
|
||||||
|
export * from "./types/types-external"
|
||||||
|
export * from "./types/types-internal"
|
||||||
|
export * from "./utils/common"
|
||||||
|
export * from "./utils/plugins"
|
||||||
|
export * from "./core/scope"
|
||||||
|
export * from "./core/finalize"
|
||||||
|
export * from "./core/proxy"
|
||||||
|
export * from "./core/immerClass"
|
||||||
|
export * from "./core/current"
|
|
@ -0,0 +1,304 @@
|
||||||
|
// types only!
|
||||||
|
import {
|
||||||
|
ImmerState,
|
||||||
|
AnyMap,
|
||||||
|
AnySet,
|
||||||
|
MapState,
|
||||||
|
SetState,
|
||||||
|
DRAFT_STATE,
|
||||||
|
getCurrentScope,
|
||||||
|
latest,
|
||||||
|
isDraftable,
|
||||||
|
createProxy,
|
||||||
|
loadPlugin,
|
||||||
|
markChanged,
|
||||||
|
die,
|
||||||
|
ArchType,
|
||||||
|
each
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
export function enableMapSet() {
|
||||||
|
class DraftMap extends Map {
|
||||||
|
[DRAFT_STATE]: MapState
|
||||||
|
|
||||||
|
constructor(target: AnyMap, parent?: ImmerState) {
|
||||||
|
super()
|
||||||
|
this[DRAFT_STATE] = {
|
||||||
|
type_: ArchType.Map,
|
||||||
|
parent_: parent,
|
||||||
|
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||||||
|
modified_: false,
|
||||||
|
finalized_: false,
|
||||||
|
copy_: undefined,
|
||||||
|
assigned_: undefined,
|
||||||
|
base_: target,
|
||||||
|
draft_: this as any,
|
||||||
|
isManual_: false,
|
||||||
|
revoked_: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return latest(this[DRAFT_STATE]).size
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: any): boolean {
|
||||||
|
return latest(this[DRAFT_STATE]).has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: any, value: any) {
|
||||||
|
const state: MapState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
if (!latest(state).has(key) || latest(state).get(key) !== value) {
|
||||||
|
prepareMapCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
state.assigned_!.set(key, true)
|
||||||
|
state.copy_!.set(key, value)
|
||||||
|
state.assigned_!.set(key, true)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: any): boolean {
|
||||||
|
if (!this.has(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const state: MapState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
prepareMapCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
if (state.base_.has(key)) {
|
||||||
|
state.assigned_!.set(key, false)
|
||||||
|
} else {
|
||||||
|
state.assigned_!.delete(key)
|
||||||
|
}
|
||||||
|
state.copy_!.delete(key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
const state: MapState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
if (latest(state).size) {
|
||||||
|
prepareMapCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
state.assigned_ = new Map()
|
||||||
|
each(state.base_, key => {
|
||||||
|
state.assigned_!.set(key, false)
|
||||||
|
})
|
||||||
|
state.copy_!.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(cb: (value: any, key: any, self: any) => void, thisArg?: any) {
|
||||||
|
const state: MapState = this[DRAFT_STATE]
|
||||||
|
latest(state).forEach((_value: any, key: any, _map: any) => {
|
||||||
|
cb.call(thisArg, this.get(key), key, this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: any): any {
|
||||||
|
const state: MapState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
const value = latest(state).get(key)
|
||||||
|
if (state.finalized_ || !isDraftable(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (value !== state.base_.get(key)) {
|
||||||
|
return value // either already drafted or reassigned
|
||||||
|
}
|
||||||
|
// despite what it looks, this creates a draft only once, see above condition
|
||||||
|
const draft = createProxy(value, state)
|
||||||
|
prepareMapCopy(state)
|
||||||
|
state.copy_!.set(key, draft)
|
||||||
|
return draft
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): IterableIterator<any> {
|
||||||
|
return latest(this[DRAFT_STATE]).keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): IterableIterator<any> {
|
||||||
|
const iterator = this.keys()
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: () => this.values(),
|
||||||
|
next: () => {
|
||||||
|
const r = iterator.next()
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (r.done) return r
|
||||||
|
const value = this.get(r.value)
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[any, any]> {
|
||||||
|
const iterator = this.keys()
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: () => this.entries(),
|
||||||
|
next: () => {
|
||||||
|
const r = iterator.next()
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (r.done) return r
|
||||||
|
const value = this.get(r.value)
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: [r.value, value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.entries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxyMap_<T extends AnyMap>(target: T, parent?: ImmerState): T {
|
||||||
|
// @ts-ignore
|
||||||
|
return new DraftMap(target, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareMapCopy(state: MapState) {
|
||||||
|
if (!state.copy_) {
|
||||||
|
state.assigned_ = new Map()
|
||||||
|
state.copy_ = new Map(state.base_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DraftSet extends Set {
|
||||||
|
[DRAFT_STATE]: SetState
|
||||||
|
constructor(target: AnySet, parent?: ImmerState) {
|
||||||
|
super()
|
||||||
|
this[DRAFT_STATE] = {
|
||||||
|
type_: ArchType.Set,
|
||||||
|
parent_: parent,
|
||||||
|
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||||||
|
modified_: false,
|
||||||
|
finalized_: false,
|
||||||
|
copy_: undefined,
|
||||||
|
base_: target,
|
||||||
|
draft_: this,
|
||||||
|
drafts_: new Map(),
|
||||||
|
revoked_: false,
|
||||||
|
isManual_: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return latest(this[DRAFT_STATE]).size
|
||||||
|
}
|
||||||
|
|
||||||
|
has(value: any): boolean {
|
||||||
|
const state: SetState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
// bit of trickery here, to be able to recognize both the value, and the draft of its value
|
||||||
|
if (!state.copy_) {
|
||||||
|
return state.base_.has(value)
|
||||||
|
}
|
||||||
|
if (state.copy_.has(value)) return true
|
||||||
|
if (state.drafts_.has(value) && state.copy_.has(state.drafts_.get(value)))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: any): any {
|
||||||
|
const state: SetState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
if (!this.has(value)) {
|
||||||
|
prepareSetCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
state.copy_!.add(value)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(value: any): any {
|
||||||
|
if (!this.has(value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const state: SetState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
prepareSetCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
return (
|
||||||
|
state.copy_!.delete(value) ||
|
||||||
|
(state.drafts_.has(value)
|
||||||
|
? state.copy_!.delete(state.drafts_.get(value))
|
||||||
|
: /* istanbul ignore next */ false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
const state: SetState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
if (latest(state).size) {
|
||||||
|
prepareSetCopy(state)
|
||||||
|
markChanged(state)
|
||||||
|
state.copy_!.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): IterableIterator<any> {
|
||||||
|
const state: SetState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
prepareSetCopy(state)
|
||||||
|
return state.copy_!.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[any, any]> {
|
||||||
|
const state: SetState = this[DRAFT_STATE]
|
||||||
|
assertUnrevoked(state)
|
||||||
|
prepareSetCopy(state)
|
||||||
|
return state.copy_!.entries()
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): IterableIterator<any> {
|
||||||
|
return this.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(cb: any, thisArg?: any) {
|
||||||
|
const iterator = this.values()
|
||||||
|
let result = iterator.next()
|
||||||
|
while (!result.done) {
|
||||||
|
cb.call(thisArg, result.value, result.value, this)
|
||||||
|
result = iterator.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function proxySet_<T extends AnySet>(target: T, parent?: ImmerState): T {
|
||||||
|
// @ts-ignore
|
||||||
|
return new DraftSet(target, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSetCopy(state: SetState) {
|
||||||
|
if (!state.copy_) {
|
||||||
|
// create drafts for all entries to preserve insertion order
|
||||||
|
state.copy_ = new Set()
|
||||||
|
state.base_.forEach(value => {
|
||||||
|
if (isDraftable(value)) {
|
||||||
|
const draft = createProxy(value, state)
|
||||||
|
state.drafts_.set(value, draft)
|
||||||
|
state.copy_!.add(draft)
|
||||||
|
} else {
|
||||||
|
state.copy_!.add(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertUnrevoked(state: any /*ES5State | MapState | SetState*/) {
|
||||||
|
if (state.revoked_) die(3, JSON.stringify(latest(state)))
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPlugin("MapSet", {proxyMap_, proxySet_})
|
||||||
|
}
|
|
@ -0,0 +1,317 @@
|
||||||
|
import {immerable} from "../immer"
|
||||||
|
import {
|
||||||
|
ImmerState,
|
||||||
|
Patch,
|
||||||
|
SetState,
|
||||||
|
ProxyArrayState,
|
||||||
|
MapState,
|
||||||
|
ProxyObjectState,
|
||||||
|
PatchPath,
|
||||||
|
get,
|
||||||
|
each,
|
||||||
|
has,
|
||||||
|
getArchtype,
|
||||||
|
getPrototypeOf,
|
||||||
|
isSet,
|
||||||
|
isMap,
|
||||||
|
loadPlugin,
|
||||||
|
ArchType,
|
||||||
|
die,
|
||||||
|
isDraft,
|
||||||
|
isDraftable,
|
||||||
|
NOTHING,
|
||||||
|
errors
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
export function enablePatches() {
|
||||||
|
const errorOffset = 16
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
errors.push(
|
||||||
|
'Sets cannot have "replace" patches.',
|
||||||
|
function(op: string) {
|
||||||
|
return "Unsupported patch operation: " + op
|
||||||
|
},
|
||||||
|
function(path: string) {
|
||||||
|
return "Cannot apply patch, path doesn't resolve: " + path
|
||||||
|
},
|
||||||
|
"Patching reserved attributes like __proto__, prototype and constructor is not allowed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPLACE = "replace"
|
||||||
|
const ADD = "add"
|
||||||
|
const REMOVE = "remove"
|
||||||
|
|
||||||
|
function generatePatches_(
|
||||||
|
state: ImmerState,
|
||||||
|
basePath: PatchPath,
|
||||||
|
patches: Patch[],
|
||||||
|
inversePatches: Patch[]
|
||||||
|
): void {
|
||||||
|
switch (state.type_) {
|
||||||
|
case ArchType.Object:
|
||||||
|
case ArchType.Map:
|
||||||
|
return generatePatchesFromAssigned(
|
||||||
|
state,
|
||||||
|
basePath,
|
||||||
|
patches,
|
||||||
|
inversePatches
|
||||||
|
)
|
||||||
|
case ArchType.Array:
|
||||||
|
return generateArrayPatches(state, basePath, patches, inversePatches)
|
||||||
|
case ArchType.Set:
|
||||||
|
return generateSetPatches(
|
||||||
|
(state as any) as SetState,
|
||||||
|
basePath,
|
||||||
|
patches,
|
||||||
|
inversePatches
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateArrayPatches(
|
||||||
|
state: ProxyArrayState,
|
||||||
|
basePath: PatchPath,
|
||||||
|
patches: Patch[],
|
||||||
|
inversePatches: Patch[]
|
||||||
|
) {
|
||||||
|
let {base_, assigned_} = state
|
||||||
|
let copy_ = state.copy_!
|
||||||
|
|
||||||
|
// Reduce complexity by ensuring `base` is never longer.
|
||||||
|
if (copy_.length < base_.length) {
|
||||||
|
// @ts-ignore
|
||||||
|
;[base_, copy_] = [copy_, base_]
|
||||||
|
;[patches, inversePatches] = [inversePatches, patches]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process replaced indices.
|
||||||
|
for (let i = 0; i < base_.length; i++) {
|
||||||
|
if (assigned_[i] && copy_[i] !== base_[i]) {
|
||||||
|
const path = basePath.concat([i])
|
||||||
|
patches.push({
|
||||||
|
op: REPLACE,
|
||||||
|
path,
|
||||||
|
// Need to maybe clone it, as it can in fact be the original value
|
||||||
|
// due to the base/copy inversion at the start of this function
|
||||||
|
value: clonePatchValueIfNeeded(copy_[i])
|
||||||
|
})
|
||||||
|
inversePatches.push({
|
||||||
|
op: REPLACE,
|
||||||
|
path,
|
||||||
|
value: clonePatchValueIfNeeded(base_[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process added indices.
|
||||||
|
for (let i = base_.length; i < copy_.length; i++) {
|
||||||
|
const path = basePath.concat([i])
|
||||||
|
patches.push({
|
||||||
|
op: ADD,
|
||||||
|
path,
|
||||||
|
// Need to maybe clone it, as it can in fact be the original value
|
||||||
|
// due to the base/copy inversion at the start of this function
|
||||||
|
value: clonePatchValueIfNeeded(copy_[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (let i = copy_.length - 1; base_.length <= i; --i) {
|
||||||
|
const path = basePath.concat([i])
|
||||||
|
inversePatches.push({
|
||||||
|
op: REMOVE,
|
||||||
|
path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used for both Map objects and normal objects.
|
||||||
|
function generatePatchesFromAssigned(
|
||||||
|
state: MapState | ProxyObjectState,
|
||||||
|
basePath: PatchPath,
|
||||||
|
patches: Patch[],
|
||||||
|
inversePatches: Patch[]
|
||||||
|
) {
|
||||||
|
const {base_, copy_} = state
|
||||||
|
each(state.assigned_!, (key, assignedValue) => {
|
||||||
|
const origValue = get(base_, key)
|
||||||
|
const value = get(copy_!, key)
|
||||||
|
const op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD
|
||||||
|
if (origValue === value && op === REPLACE) return
|
||||||
|
const path = basePath.concat(key as any)
|
||||||
|
patches.push(op === REMOVE ? {op, path} : {op, path, value})
|
||||||
|
inversePatches.push(
|
||||||
|
op === ADD
|
||||||
|
? {op: REMOVE, path}
|
||||||
|
: op === REMOVE
|
||||||
|
? {op: ADD, path, value: clonePatchValueIfNeeded(origValue)}
|
||||||
|
: {op: REPLACE, path, value: clonePatchValueIfNeeded(origValue)}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSetPatches(
|
||||||
|
state: SetState,
|
||||||
|
basePath: PatchPath,
|
||||||
|
patches: Patch[],
|
||||||
|
inversePatches: Patch[]
|
||||||
|
) {
|
||||||
|
let {base_, copy_} = state
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
base_.forEach((value: any) => {
|
||||||
|
if (!copy_!.has(value)) {
|
||||||
|
const path = basePath.concat([i])
|
||||||
|
patches.push({
|
||||||
|
op: REMOVE,
|
||||||
|
path,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
inversePatches.unshift({
|
||||||
|
op: ADD,
|
||||||
|
path,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
})
|
||||||
|
i = 0
|
||||||
|
copy_!.forEach((value: any) => {
|
||||||
|
if (!base_.has(value)) {
|
||||||
|
const path = basePath.concat([i])
|
||||||
|
patches.push({
|
||||||
|
op: ADD,
|
||||||
|
path,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
inversePatches.unshift({
|
||||||
|
op: REMOVE,
|
||||||
|
path,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateReplacementPatches_(
|
||||||
|
baseValue: any,
|
||||||
|
replacement: any,
|
||||||
|
patches: Patch[],
|
||||||
|
inversePatches: Patch[]
|
||||||
|
): void {
|
||||||
|
patches.push({
|
||||||
|
op: REPLACE,
|
||||||
|
path: [],
|
||||||
|
value: replacement === NOTHING ? undefined : replacement
|
||||||
|
})
|
||||||
|
inversePatches.push({
|
||||||
|
op: REPLACE,
|
||||||
|
path: [],
|
||||||
|
value: baseValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatches_<T>(draft: T, patches: readonly Patch[]): T {
|
||||||
|
patches.forEach(patch => {
|
||||||
|
const {path, op} = patch
|
||||||
|
|
||||||
|
let base: any = draft
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
const parentType = getArchtype(base)
|
||||||
|
let p = path[i]
|
||||||
|
if (typeof p !== "string" && typeof p !== "number") {
|
||||||
|
p = "" + p
|
||||||
|
}
|
||||||
|
|
||||||
|
// See #738, avoid prototype pollution
|
||||||
|
if (
|
||||||
|
(parentType === ArchType.Object || parentType === ArchType.Array) &&
|
||||||
|
(p === "__proto__" || p === "constructor")
|
||||||
|
)
|
||||||
|
die(errorOffset + 3)
|
||||||
|
if (typeof base === "function" && p === "prototype")
|
||||||
|
die(errorOffset + 3)
|
||||||
|
base = get(base, p)
|
||||||
|
if (typeof base !== "object") die(errorOffset + 2, path.join("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = getArchtype(base)
|
||||||
|
const value = deepClonePatchValue(patch.value) // used to clone patch to ensure original patch is not modified, see #411
|
||||||
|
const key = path[path.length - 1]
|
||||||
|
switch (op) {
|
||||||
|
case REPLACE:
|
||||||
|
switch (type) {
|
||||||
|
case ArchType.Map:
|
||||||
|
return base.set(key, value)
|
||||||
|
/* istanbul ignore next */
|
||||||
|
case ArchType.Set:
|
||||||
|
die(errorOffset)
|
||||||
|
default:
|
||||||
|
// if value is an object, then it's assigned by reference
|
||||||
|
// in the following add or remove ops, the value field inside the patch will also be modifyed
|
||||||
|
// so we use value from the cloned patch
|
||||||
|
// @ts-ignore
|
||||||
|
return (base[key] = value)
|
||||||
|
}
|
||||||
|
case ADD:
|
||||||
|
switch (type) {
|
||||||
|
case ArchType.Array:
|
||||||
|
return key === "-"
|
||||||
|
? base.push(value)
|
||||||
|
: base.splice(key as any, 0, value)
|
||||||
|
case ArchType.Map:
|
||||||
|
return base.set(key, value)
|
||||||
|
case ArchType.Set:
|
||||||
|
return base.add(value)
|
||||||
|
default:
|
||||||
|
return (base[key] = value)
|
||||||
|
}
|
||||||
|
case REMOVE:
|
||||||
|
switch (type) {
|
||||||
|
case ArchType.Array:
|
||||||
|
return base.splice(key as any, 1)
|
||||||
|
case ArchType.Map:
|
||||||
|
return base.delete(key)
|
||||||
|
case ArchType.Set:
|
||||||
|
return base.delete(patch.value)
|
||||||
|
default:
|
||||||
|
return delete base[key]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
die(errorOffset + 1, op)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return draft
|
||||||
|
}
|
||||||
|
|
||||||
|
// optimize: this is quite a performance hit, can we detect intelligently when it is needed?
|
||||||
|
// E.g. auto-draft when new objects from outside are assigned and modified?
|
||||||
|
// (See failing test when deepClone just returns obj)
|
||||||
|
function deepClonePatchValue<T>(obj: T): T
|
||||||
|
function deepClonePatchValue(obj: any) {
|
||||||
|
if (!isDraftable(obj)) return obj
|
||||||
|
if (Array.isArray(obj)) return obj.map(deepClonePatchValue)
|
||||||
|
if (isMap(obj))
|
||||||
|
return new Map(
|
||||||
|
Array.from(obj.entries()).map(([k, v]) => [k, deepClonePatchValue(v)])
|
||||||
|
)
|
||||||
|
if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue))
|
||||||
|
const cloned = Object.create(getPrototypeOf(obj))
|
||||||
|
for (const key in obj) cloned[key] = deepClonePatchValue(obj[key])
|
||||||
|
if (has(obj, immerable)) cloned[immerable] = obj[immerable]
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
function clonePatchValueIfNeeded<T>(obj: T): T {
|
||||||
|
if (isDraft(obj)) {
|
||||||
|
return deepClonePatchValue(obj)
|
||||||
|
} else return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPlugin("Patches", {
|
||||||
|
applyPatches_,
|
||||||
|
generatePatches_,
|
||||||
|
generateReplacementPatches_
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
declare const __DEV__: boolean
|
|
@ -0,0 +1,111 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export interface Patch {
|
||||||
|
op: "replace" | "remove" | "add";
|
||||||
|
path: (string | number)[];
|
||||||
|
value?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void
|
||||||
|
|
||||||
|
type Base = {...} | Array<any>
|
||||||
|
interface IProduce {
|
||||||
|
/**
|
||||||
|
* Immer takes a state, and runs a function against it.
|
||||||
|
* That function can freely mutate the state, as it will create copies-on-write.
|
||||||
|
* This means that the original state will stay unchanged, and once the function finishes, the modified state is returned.
|
||||||
|
*
|
||||||
|
* If the first argument is a function, this is interpreted as the recipe, and will create a curried function that will execute the recipe
|
||||||
|
* any time it is called with the current state.
|
||||||
|
*
|
||||||
|
* @param currentState - the state to start with
|
||||||
|
* @param recipe - function that receives a proxy of the current state as first argument and which can be freely modified
|
||||||
|
* @param initialState - if a curried function is created and this argument was given, it will be used as fallback if the curried function is called with a state of undefined
|
||||||
|
* @returns The next state: a new state, or the current state if nothing was modified
|
||||||
|
*/
|
||||||
|
<S: Base>(
|
||||||
|
currentState: S,
|
||||||
|
recipe: (draftState: S) => S | void,
|
||||||
|
patchListener?: PatchListener
|
||||||
|
): S;
|
||||||
|
// curried invocations with initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void,
|
||||||
|
initialState: S
|
||||||
|
): (currentState: S | void, a: A, b: B, c: C, ...extraArgs: any[]) => S;
|
||||||
|
// curried invocations without initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void
|
||||||
|
): (currentState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProduceWithPatches {
|
||||||
|
/**
|
||||||
|
* Like `produce`, but instead of just returning the new state,
|
||||||
|
* a tuple is returned with [nextState, patches, inversePatches]
|
||||||
|
*
|
||||||
|
* Like produce, this function supports currying
|
||||||
|
*/
|
||||||
|
<S: Base>(
|
||||||
|
currentState: S,
|
||||||
|
recipe: (draftState: S) => S | void
|
||||||
|
): [S, Patch[], Patch[]];
|
||||||
|
// curried invocations with initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void,
|
||||||
|
initialState: S
|
||||||
|
): (currentState: S | void, a: A, b: B, c: C, ...extraArgs: any[]) => [S, Patch[], Patch[]];
|
||||||
|
// curried invocations without initial state
|
||||||
|
<S: Base, A = void, B = void, C = void>(
|
||||||
|
recipe: (draftState: S, a: A, b: B, c: C, ...extraArgs: any[]) => S | void
|
||||||
|
): (currentState: S, a: A, b: B, c: C, ...extraArgs: any[]) => [S, Patch[], Patch[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare export var produce: IProduce
|
||||||
|
|
||||||
|
declare export var produceWithPatches: IProduceWithPatches
|
||||||
|
|
||||||
|
declare export var nothing: typeof undefined
|
||||||
|
|
||||||
|
declare export var immerable: Symbol
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically freezes any state trees generated by immer.
|
||||||
|
* This protects against accidental modifications of the state tree outside of an immer function.
|
||||||
|
* This comes with a performance impact, so it is recommended to disable this option in production.
|
||||||
|
* By default it is turned on during local development, and turned off in production.
|
||||||
|
*/
|
||||||
|
declare export function setAutoFreeze(autoFreeze: boolean): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass false to disable strict shallow copy.
|
||||||
|
*
|
||||||
|
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
||||||
|
*/
|
||||||
|
declare export function setUseStrictShallowCopy(useStrictShallowCopy: boolean): void
|
||||||
|
|
||||||
|
declare export function applyPatches<S>(state: S, patches: Patch[]): S
|
||||||
|
|
||||||
|
declare export function original<S>(value: S): S
|
||||||
|
|
||||||
|
declare export function current<S>(value: S): S
|
||||||
|
|
||||||
|
declare export function isDraft(value: any): boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mutable draft from an (immutable) object / array.
|
||||||
|
* The draft can be modified until `finishDraft` is called
|
||||||
|
*/
|
||||||
|
declare export function createDraft<T>(base: T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a draft that was created using `createDraft`,
|
||||||
|
* finalizes the draft into a new immutable object.
|
||||||
|
* Optionally a patch-listener can be provided to gather the patches that are needed to construct the object.
|
||||||
|
*/
|
||||||
|
declare export function finishDraft<T>(base: T, listener?: PatchListener): T
|
||||||
|
|
||||||
|
declare export function enableMapSet(): void
|
||||||
|
declare export function enablePatches(): void
|
||||||
|
|
||||||
|
declare export function freeze<T>(obj: T, freeze?: boolean): T
|
|
@ -0,0 +1,239 @@
|
||||||
|
import {NOTHING} from "../internal"
|
||||||
|
|
||||||
|
type AnyFunc = (...args: any[]) => any
|
||||||
|
|
||||||
|
type PrimitiveType = number | string | boolean
|
||||||
|
|
||||||
|
/** Object types that should never be mapped */
|
||||||
|
type AtomicObject = Function | Promise<any> | Date | RegExp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the lib "ES2015.Collection" is not included in tsconfig.json,
|
||||||
|
* types like ReadonlyArray, WeakMap etc. fall back to `any` (specified nowhere)
|
||||||
|
* or `{}` (from the node types), in both cases entering an infinite recursion in
|
||||||
|
* pattern matching type mappings
|
||||||
|
* This type can be used to cast these types to `void` in these cases.
|
||||||
|
*/
|
||||||
|
export type IfAvailable<T, Fallback = void> =
|
||||||
|
// fallback if any
|
||||||
|
true | false extends (T extends never
|
||||||
|
? true
|
||||||
|
: false)
|
||||||
|
? Fallback // fallback if empty type
|
||||||
|
: keyof T extends never
|
||||||
|
? Fallback // original type
|
||||||
|
: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These should also never be mapped but must be tested after regular Map and
|
||||||
|
* Set
|
||||||
|
*/
|
||||||
|
type WeakReferences = IfAvailable<WeakMap<any, any>> | IfAvailable<WeakSet<any>>
|
||||||
|
|
||||||
|
export type WritableDraft<T> = {-readonly [K in keyof T]: Draft<T[K]>}
|
||||||
|
|
||||||
|
/** Convert a readonly type into a mutable type, if possible */
|
||||||
|
export type Draft<T> = T extends PrimitiveType
|
||||||
|
? T
|
||||||
|
: T extends AtomicObject
|
||||||
|
? T
|
||||||
|
: T extends ReadonlyMap<infer K, infer V> // Map extends ReadonlyMap
|
||||||
|
? Map<Draft<K>, Draft<V>>
|
||||||
|
: T extends ReadonlySet<infer V> // Set extends ReadonlySet
|
||||||
|
? Set<Draft<V>>
|
||||||
|
: T extends WeakReferences
|
||||||
|
? T
|
||||||
|
: T extends object
|
||||||
|
? WritableDraft<T>
|
||||||
|
: T
|
||||||
|
|
||||||
|
/** Convert a mutable type into a readonly type */
|
||||||
|
export type Immutable<T> = T extends PrimitiveType
|
||||||
|
? T
|
||||||
|
: T extends AtomicObject
|
||||||
|
? T
|
||||||
|
: T extends ReadonlyMap<infer K, infer V> // Map extends ReadonlyMap
|
||||||
|
? ReadonlyMap<Immutable<K>, Immutable<V>>
|
||||||
|
: T extends ReadonlySet<infer V> // Set extends ReadonlySet
|
||||||
|
? ReadonlySet<Immutable<V>>
|
||||||
|
: T extends WeakReferences
|
||||||
|
? T
|
||||||
|
: T extends object
|
||||||
|
? {readonly [K in keyof T]: Immutable<T[K]>}
|
||||||
|
: T
|
||||||
|
|
||||||
|
export interface Patch {
|
||||||
|
op: "replace" | "remove" | "add"
|
||||||
|
path: (string | number)[]
|
||||||
|
value?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void
|
||||||
|
|
||||||
|
/** Converts `nothing` into `undefined` */
|
||||||
|
type FromNothing<T> = T extends typeof NOTHING ? undefined : T
|
||||||
|
|
||||||
|
/** The inferred return type of `produce` */
|
||||||
|
export type Produced<Base, Return> = Return extends void
|
||||||
|
? Base
|
||||||
|
: FromNothing<Return>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility types
|
||||||
|
*/
|
||||||
|
type PatchesTuple<T> = readonly [T, Patch[], Patch[]]
|
||||||
|
|
||||||
|
type ValidRecipeReturnType<State> =
|
||||||
|
| State
|
||||||
|
| void
|
||||||
|
| undefined
|
||||||
|
| (State extends undefined ? typeof NOTHING : never)
|
||||||
|
|
||||||
|
type ReturnTypeWithPatchesIfNeeded<
|
||||||
|
State,
|
||||||
|
UsePatches extends boolean
|
||||||
|
> = UsePatches extends true ? PatchesTuple<State> : State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core Producer inference
|
||||||
|
*/
|
||||||
|
type InferRecipeFromCurried<Curried> = Curried extends (
|
||||||
|
base: infer State,
|
||||||
|
...rest: infer Args
|
||||||
|
) => any // extra assertion to make sure this is a proper curried function (state, args) => state
|
||||||
|
? ReturnType<Curried> extends State
|
||||||
|
? (
|
||||||
|
draft: Draft<State>,
|
||||||
|
...rest: Args
|
||||||
|
) => ValidRecipeReturnType<Draft<State>>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
type InferInitialStateFromCurried<Curried> = Curried extends (
|
||||||
|
base: infer State,
|
||||||
|
...rest: any[]
|
||||||
|
) => any // extra assertion to make sure this is a proper curried function (state, args) => state
|
||||||
|
? State
|
||||||
|
: never
|
||||||
|
|
||||||
|
type InferCurriedFromRecipe<
|
||||||
|
Recipe,
|
||||||
|
UsePatches extends boolean
|
||||||
|
> = Recipe extends (draft: infer DraftState, ...args: infer RestArgs) => any // verify return type
|
||||||
|
? ReturnType<Recipe> extends ValidRecipeReturnType<DraftState>
|
||||||
|
? (
|
||||||
|
base: Immutable<DraftState>,
|
||||||
|
...args: RestArgs
|
||||||
|
) => ReturnTypeWithPatchesIfNeeded<DraftState, UsePatches> // N.b. we return mutable draftstate, in case the recipe's first arg isn't read only, and that isn't expected as output either
|
||||||
|
: never // incorrect return type
|
||||||
|
: never // not a function
|
||||||
|
|
||||||
|
type InferCurriedFromInitialStateAndRecipe<
|
||||||
|
State,
|
||||||
|
Recipe,
|
||||||
|
UsePatches extends boolean
|
||||||
|
> = Recipe extends (
|
||||||
|
draft: Draft<State>,
|
||||||
|
...rest: infer RestArgs
|
||||||
|
) => ValidRecipeReturnType<State>
|
||||||
|
? (
|
||||||
|
base?: State | undefined,
|
||||||
|
...args: RestArgs
|
||||||
|
) => ReturnTypeWithPatchesIfNeeded<State, UsePatches>
|
||||||
|
: never // recipe doesn't match initial state
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `produce` function takes a value and a "recipe function" (whose
|
||||||
|
* return value often depends on the base state). The recipe function is
|
||||||
|
* free to mutate its first argument however it wants. All mutations are
|
||||||
|
* only ever applied to a __copy__ of the base state.
|
||||||
|
*
|
||||||
|
* Pass only a function to create a "curried producer" which relieves you
|
||||||
|
* from passing the recipe function every time.
|
||||||
|
*
|
||||||
|
* Only plain objects and arrays are made mutable. All other objects are
|
||||||
|
* considered uncopyable.
|
||||||
|
*
|
||||||
|
* Note: This function is __bound__ to its `Immer` instance.
|
||||||
|
*
|
||||||
|
* @param {any} base - the initial state
|
||||||
|
* @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
|
||||||
|
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||||||
|
* @returns {any} a new state, or the initial state if nothing was modified
|
||||||
|
*/
|
||||||
|
export interface IProduce {
|
||||||
|
/** Curried producer that infers the recipe from the curried output function (e.g. when passing to setState) */
|
||||||
|
<Curried>(
|
||||||
|
recipe: InferRecipeFromCurried<Curried>,
|
||||||
|
initialState?: InferInitialStateFromCurried<Curried>
|
||||||
|
): Curried
|
||||||
|
|
||||||
|
/** Curried producer that infers curried from the recipe */
|
||||||
|
<Recipe extends AnyFunc>(recipe: Recipe): InferCurriedFromRecipe<
|
||||||
|
Recipe,
|
||||||
|
false
|
||||||
|
>
|
||||||
|
|
||||||
|
/** Curried producer that infers curried from the State generic, which is explicitly passed in. */
|
||||||
|
<State>(
|
||||||
|
recipe: (
|
||||||
|
state: Draft<State>,
|
||||||
|
initialState: State
|
||||||
|
) => ValidRecipeReturnType<State>
|
||||||
|
): (state?: State) => State
|
||||||
|
<State, Args extends any[]>(
|
||||||
|
recipe: (
|
||||||
|
state: Draft<State>,
|
||||||
|
...args: Args
|
||||||
|
) => ValidRecipeReturnType<State>,
|
||||||
|
initialState: State
|
||||||
|
): (state?: State, ...args: Args) => State
|
||||||
|
<State>(recipe: (state: Draft<State>) => ValidRecipeReturnType<State>): (
|
||||||
|
state: State
|
||||||
|
) => State
|
||||||
|
<State, Args extends any[]>(
|
||||||
|
recipe: (state: Draft<State>, ...args: Args) => ValidRecipeReturnType<State>
|
||||||
|
): (state: State, ...args: Args) => State
|
||||||
|
|
||||||
|
/** Curried producer with initial state, infers recipe from initial state */
|
||||||
|
<State, Recipe extends Function>(
|
||||||
|
recipe: Recipe,
|
||||||
|
initialState: State
|
||||||
|
): InferCurriedFromInitialStateAndRecipe<State, Recipe, false>
|
||||||
|
|
||||||
|
/** Normal producer */
|
||||||
|
<Base, D = Draft<Base>>( // By using a default inferred D, rather than Draft<Base> in the recipe, we can override it.
|
||||||
|
base: Base,
|
||||||
|
recipe: (draft: D) => ValidRecipeReturnType<D>,
|
||||||
|
listener?: PatchListener
|
||||||
|
): Base
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `produce`, but instead of just returning the new state,
|
||||||
|
* a tuple is returned with [nextState, patches, inversePatches]
|
||||||
|
*
|
||||||
|
* Like produce, this function supports currying
|
||||||
|
*/
|
||||||
|
export interface IProduceWithPatches {
|
||||||
|
// Types copied from IProduce, wrapped with PatchesTuple
|
||||||
|
<Recipe extends AnyFunc>(recipe: Recipe): InferCurriedFromRecipe<Recipe, true>
|
||||||
|
<State, Recipe extends Function>(
|
||||||
|
recipe: Recipe,
|
||||||
|
initialState: State
|
||||||
|
): InferCurriedFromInitialStateAndRecipe<State, Recipe, true>
|
||||||
|
<Base, D = Draft<Base>>(
|
||||||
|
base: Base,
|
||||||
|
recipe: (draft: D) => ValidRecipeReturnType<D>,
|
||||||
|
listener?: PatchListener
|
||||||
|
): PatchesTuple<Base>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type for `recipe function`
|
||||||
|
*/
|
||||||
|
export type Producer<T> = (draft: Draft<T>) => ValidRecipeReturnType<Draft<T>>
|
||||||
|
|
||||||
|
// Fixes #507: bili doesn't export the types of this file if there is no actual source in it..
|
||||||
|
// hopefully it get's tree-shaken away for everyone :)
|
||||||
|
export function never_used() {}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {
|
||||||
|
SetState,
|
||||||
|
ImmerScope,
|
||||||
|
ProxyObjectState,
|
||||||
|
ProxyArrayState,
|
||||||
|
MapState,
|
||||||
|
DRAFT_STATE
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
export type Objectish = AnyObject | AnyArray | AnyMap | AnySet
|
||||||
|
export type ObjectishNoSet = AnyObject | AnyArray | AnyMap
|
||||||
|
|
||||||
|
export type AnyObject = {[key: string]: any}
|
||||||
|
export type AnyArray = Array<any>
|
||||||
|
export type AnySet = Set<any>
|
||||||
|
export type AnyMap = Map<any, any>
|
||||||
|
|
||||||
|
export const enum ArchType {
|
||||||
|
Object,
|
||||||
|
Array,
|
||||||
|
Map,
|
||||||
|
Set
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImmerBaseState {
|
||||||
|
parent_?: ImmerState
|
||||||
|
scope_: ImmerScope
|
||||||
|
modified_: boolean
|
||||||
|
finalized_: boolean
|
||||||
|
isManual_: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImmerState =
|
||||||
|
| ProxyObjectState
|
||||||
|
| ProxyArrayState
|
||||||
|
| MapState
|
||||||
|
| SetState
|
||||||
|
|
||||||
|
// The _internal_ type used for drafts (not to be confused with Draft, which is public facing)
|
||||||
|
export type Drafted<Base = any, T extends ImmerState = ImmerState> = {
|
||||||
|
[DRAFT_STATE]: T
|
||||||
|
} & Base
|
|
@ -0,0 +1,217 @@
|
||||||
|
import {
|
||||||
|
DRAFT_STATE,
|
||||||
|
DRAFTABLE,
|
||||||
|
Objectish,
|
||||||
|
Drafted,
|
||||||
|
AnyObject,
|
||||||
|
AnyMap,
|
||||||
|
AnySet,
|
||||||
|
ImmerState,
|
||||||
|
ArchType,
|
||||||
|
die,
|
||||||
|
StrictMode
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
export const getPrototypeOf = Object.getPrototypeOf
|
||||||
|
|
||||||
|
/** Returns true if the given value is an Immer draft */
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function isDraft(value: any): boolean {
|
||||||
|
return !!value && !!value[DRAFT_STATE]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the given value can be drafted by Immer */
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function isDraftable(value: any): boolean {
|
||||||
|
if (!value) return false
|
||||||
|
return (
|
||||||
|
isPlainObject(value) ||
|
||||||
|
Array.isArray(value) ||
|
||||||
|
!!value[DRAFTABLE] ||
|
||||||
|
!!value.constructor?.[DRAFTABLE] ||
|
||||||
|
isMap(value) ||
|
||||||
|
isSet(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectCtorString = Object.prototype.constructor.toString()
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function isPlainObject(value: any): boolean {
|
||||||
|
if (!value || typeof value !== "object") return false
|
||||||
|
const proto = getPrototypeOf(value)
|
||||||
|
if (proto === null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const Ctor =
|
||||||
|
Object.hasOwnProperty.call(proto, "constructor") && proto.constructor
|
||||||
|
|
||||||
|
if (Ctor === Object) return true
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof Ctor == "function" &&
|
||||||
|
Function.toString.call(Ctor) === objectCtorString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the underlying object that is represented by the given draft */
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function original<T>(value: T): T | undefined
|
||||||
|
export function original(value: Drafted<any>): any {
|
||||||
|
if (!isDraft(value)) die(15, value)
|
||||||
|
return value[DRAFT_STATE].base_
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each iterates a map, set or array.
|
||||||
|
* Or, if any other kind of object, all of its own properties.
|
||||||
|
* Regardless whether they are enumerable or symbols
|
||||||
|
*/
|
||||||
|
export function each<T extends Objectish>(
|
||||||
|
obj: T,
|
||||||
|
iter: (key: string | number, value: any, source: T) => void
|
||||||
|
): void
|
||||||
|
export function each(obj: any, iter: any) {
|
||||||
|
if (getArchtype(obj) === ArchType.Object) {
|
||||||
|
Reflect.ownKeys(obj).forEach(key => {
|
||||||
|
iter(key, obj[key], obj)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
obj.forEach((entry: any, index: any) => iter(index, entry, obj))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function getArchtype(thing: any): ArchType {
|
||||||
|
const state: undefined | ImmerState = thing[DRAFT_STATE]
|
||||||
|
return state
|
||||||
|
? state.type_
|
||||||
|
: Array.isArray(thing)
|
||||||
|
? ArchType.Array
|
||||||
|
: isMap(thing)
|
||||||
|
? ArchType.Map
|
||||||
|
: isSet(thing)
|
||||||
|
? ArchType.Set
|
||||||
|
: ArchType.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function has(thing: any, prop: PropertyKey): boolean {
|
||||||
|
return getArchtype(thing) === ArchType.Map
|
||||||
|
? thing.has(prop)
|
||||||
|
: Object.prototype.hasOwnProperty.call(thing, prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function get(thing: AnyMap | AnyObject, prop: PropertyKey): any {
|
||||||
|
// @ts-ignore
|
||||||
|
return getArchtype(thing) === ArchType.Map ? thing.get(prop) : thing[prop]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function set(thing: any, propOrOldValue: PropertyKey, value: any) {
|
||||||
|
const t = getArchtype(thing)
|
||||||
|
if (t === ArchType.Map) thing.set(propOrOldValue, value)
|
||||||
|
else if (t === ArchType.Set) {
|
||||||
|
thing.add(value)
|
||||||
|
} else thing[propOrOldValue] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function is(x: any, y: any): boolean {
|
||||||
|
// From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js
|
||||||
|
if (x === y) {
|
||||||
|
return x !== 0 || 1 / x === 1 / y
|
||||||
|
} else {
|
||||||
|
return x !== x && y !== y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function isMap(target: any): target is AnyMap {
|
||||||
|
return target instanceof Map
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function isSet(target: any): target is AnySet {
|
||||||
|
return target instanceof Set
|
||||||
|
}
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function latest(state: ImmerState): any {
|
||||||
|
return state.copy_ || state.base_
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#__PURE__*/
|
||||||
|
export function shallowCopy(base: any, strict: StrictMode) {
|
||||||
|
if (isMap(base)) {
|
||||||
|
return new Map(base)
|
||||||
|
}
|
||||||
|
if (isSet(base)) {
|
||||||
|
return new Set(base)
|
||||||
|
}
|
||||||
|
if (Array.isArray(base)) return Array.prototype.slice.call(base)
|
||||||
|
|
||||||
|
const isPlain = isPlainObject(base)
|
||||||
|
|
||||||
|
if (strict === true || (strict === "class_only" && !isPlain)) {
|
||||||
|
// Perform a strict copy
|
||||||
|
const descriptors = Object.getOwnPropertyDescriptors(base)
|
||||||
|
delete descriptors[DRAFT_STATE as any]
|
||||||
|
let keys = Reflect.ownKeys(descriptors)
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key: any = keys[i]
|
||||||
|
const desc = descriptors[key]
|
||||||
|
if (desc.writable === false) {
|
||||||
|
desc.writable = true
|
||||||
|
desc.configurable = true
|
||||||
|
}
|
||||||
|
// like object.assign, we will read any _own_, get/set accessors. This helps in dealing
|
||||||
|
// with libraries that trap values, like mobx or vue
|
||||||
|
// unlike object.assign, non-enumerables will be copied as well
|
||||||
|
if (desc.get || desc.set)
|
||||||
|
descriptors[key] = {
|
||||||
|
configurable: true,
|
||||||
|
writable: true, // could live with !!desc.set as well here...
|
||||||
|
enumerable: desc.enumerable,
|
||||||
|
value: base[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.create(getPrototypeOf(base), descriptors)
|
||||||
|
} else {
|
||||||
|
// perform a sloppy copy
|
||||||
|
const proto = getPrototypeOf(base)
|
||||||
|
if (proto !== null && isPlain) {
|
||||||
|
return {...base} // assumption: better inner class optimization than the assign below
|
||||||
|
}
|
||||||
|
const obj = Object.create(proto)
|
||||||
|
return Object.assign(obj, base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freezes draftable objects. Returns the original object.
|
||||||
|
* By default freezes shallowly, but if the second argument is `true` it will freeze recursively.
|
||||||
|
*
|
||||||
|
* @param obj
|
||||||
|
* @param deep
|
||||||
|
*/
|
||||||
|
export function freeze<T>(obj: T, deep?: boolean): T
|
||||||
|
export function freeze<T>(obj: any, deep: boolean = false): T {
|
||||||
|
if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj
|
||||||
|
if (getArchtype(obj) > 1 /* Map or Set */) {
|
||||||
|
obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections as any
|
||||||
|
}
|
||||||
|
Object.freeze(obj)
|
||||||
|
if (deep)
|
||||||
|
// See #590, don't recurse into non-enumerable / Symbol properties when freezing
|
||||||
|
// So use Object.entries (only string-like, enumerables) instead of each()
|
||||||
|
Object.entries(obj).forEach(([key, value]) => freeze(value, true))
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
function dontMutateFrozenCollections() {
|
||||||
|
die(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFrozen(obj: any): boolean {
|
||||||
|
return Object.isFrozen(obj)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Should be no imports here!
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sentinel value returned by producers to replace the draft with undefined.
|
||||||
|
*/
|
||||||
|
export const NOTHING: unique symbol = Symbol.for("immer-nothing")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To let Immer treat your class instances as plain immutable objects
|
||||||
|
* (albeit with a custom prototype), you must define either an instance property
|
||||||
|
* or a static property on each of your custom classes.
|
||||||
|
*
|
||||||
|
* Otherwise, your class instance will never be drafted, which means it won't be
|
||||||
|
* safe to mutate in a produce callback.
|
||||||
|
*/
|
||||||
|
export const DRAFTABLE: unique symbol = Symbol.for("immer-draftable")
|
||||||
|
|
||||||
|
export const DRAFT_STATE: unique symbol = Symbol.for("immer-state")
|
|
@ -0,0 +1,48 @@
|
||||||
|
export const errors =
|
||||||
|
process.env.NODE_ENV !== "production"
|
||||||
|
? [
|
||||||
|
// All error codes, starting by 0:
|
||||||
|
function(plugin: string) {
|
||||||
|
return `The plugin for '${plugin}' has not been loaded into Immer. To enable the plugin, import and call \`enable${plugin}()\` when initializing your application.`
|
||||||
|
},
|
||||||
|
function(thing: string) {
|
||||||
|
return `produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '${thing}'`
|
||||||
|
},
|
||||||
|
"This object has been frozen and should not be mutated",
|
||||||
|
function(data: any) {
|
||||||
|
return (
|
||||||
|
"Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " +
|
||||||
|
data
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.",
|
||||||
|
"Immer forbids circular references",
|
||||||
|
"The first or second argument to `produce` must be a function",
|
||||||
|
"The third argument to `produce` must be a function or undefined",
|
||||||
|
"First argument to `createDraft` must be a plain object, an array, or an immerable object",
|
||||||
|
"First argument to `finishDraft` must be a draft returned by `createDraft`",
|
||||||
|
function(thing: string) {
|
||||||
|
return `'current' expects a draft, got: ${thing}`
|
||||||
|
},
|
||||||
|
"Object.defineProperty() cannot be used on an Immer draft",
|
||||||
|
"Object.setPrototypeOf() cannot be used on an Immer draft",
|
||||||
|
"Immer only supports deleting array indices",
|
||||||
|
"Immer only supports setting array indices and the 'length' property",
|
||||||
|
function(thing: string) {
|
||||||
|
return `'original' expects a draft, got: ${thing}`
|
||||||
|
}
|
||||||
|
// Note: if more errors are added, the errorOffset in Patches.ts should be increased
|
||||||
|
// See Patches.ts for additional errors
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
|
export function die(error: number, ...args: any[]): never {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
const e = errors[error]
|
||||||
|
const msg = typeof e === "function" ? e.apply(null, args as any) : e
|
||||||
|
throw new Error(`[Immer] ${msg}`)
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`[Immer] minified error nr: ${error}. Full error at: https://bit.ly/3cXEKWf`
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
ImmerState,
|
||||||
|
Patch,
|
||||||
|
Drafted,
|
||||||
|
ImmerBaseState,
|
||||||
|
AnyMap,
|
||||||
|
AnySet,
|
||||||
|
ArchType,
|
||||||
|
die
|
||||||
|
} from "../internal"
|
||||||
|
|
||||||
|
/** Plugin utilities */
|
||||||
|
const plugins: {
|
||||||
|
Patches?: {
|
||||||
|
generatePatches_(
|
||||||
|
state: ImmerState,
|
||||||
|
basePath: PatchPath,
|
||||||
|
patches: Patch[],
|
||||||
|
inversePatches: Patch[]
|
||||||
|
): void
|
||||||
|
generateReplacementPatches_(
|
||||||
|
base: any,
|
||||||
|
replacement: any,
|
||||||
|
patches: Patch[],
|
||||||
|
inversePatches: Patch[]
|
||||||
|
): void
|
||||||
|
applyPatches_<T>(draft: T, patches: readonly Patch[]): T
|
||||||
|
}
|
||||||
|
MapSet?: {
|
||||||
|
proxyMap_<T extends AnyMap>(target: T, parent?: ImmerState): T
|
||||||
|
proxySet_<T extends AnySet>(target: T, parent?: ImmerState): T
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
type Plugins = typeof plugins
|
||||||
|
|
||||||
|
export function getPlugin<K extends keyof Plugins>(
|
||||||
|
pluginKey: K
|
||||||
|
): Exclude<Plugins[K], undefined> {
|
||||||
|
const plugin = plugins[pluginKey]
|
||||||
|
if (!plugin) {
|
||||||
|
die(0, pluginKey)
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPlugin<K extends keyof Plugins>(
|
||||||
|
pluginKey: K,
|
||||||
|
implementation: Plugins[K]
|
||||||
|
): void {
|
||||||
|
if (!plugins[pluginKey]) plugins[pluginKey] = implementation
|
||||||
|
}
|
||||||
|
/** Map / Set plugin */
|
||||||
|
|
||||||
|
export interface MapState extends ImmerBaseState {
|
||||||
|
type_: ArchType.Map
|
||||||
|
copy_: AnyMap | undefined
|
||||||
|
assigned_: Map<any, boolean> | undefined
|
||||||
|
base_: AnyMap
|
||||||
|
revoked_: boolean
|
||||||
|
draft_: Drafted<AnyMap, MapState>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetState extends ImmerBaseState {
|
||||||
|
type_: ArchType.Set
|
||||||
|
copy_: AnySet | undefined
|
||||||
|
base_: AnySet
|
||||||
|
drafts_: Map<any, Drafted> // maps the original value to the draft value in the new set
|
||||||
|
revoked_: boolean
|
||||||
|
draft_: Drafted<AnySet, SetState>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Patches plugin */
|
||||||
|
|
||||||
|
export type PatchPath = (string | number)[]
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "certimate",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"immer": "^10.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"immer": "^10.1.1"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,8 +5,8 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DpHAV802.js"></script>
|
<script type="module" crossorigin src="/assets/index-DbwFzZm1.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DOft-CKV.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CWUb5Xuf.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background">
|
<body class="bg-background">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.417.0",
|
"lucide-react": "^0.417.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"pocketbase": "^0.21.4",
|
"pocketbase": "^0.21.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
@ -4159,9 +4160,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.0.7.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
@ -4169,10 +4170,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^18 || >=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
|
@ -4561,6 +4562,23 @@
|
||||||
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss/node_modules/nanoid": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
|
@ -29,22 +29,23 @@
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"i18next": "^23.15.1",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
|
"i18next-http-backend": "^2.6.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.417.0",
|
"lucide-react": "^0.417.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"pocketbase": "^0.21.4",
|
"pocketbase": "^0.21.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
|
"react-i18next": "^15.0.2",
|
||||||
"react-router-dom": "^6.25.1",
|
"react-router-dom": "^6.25.1",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^0.9.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8"
|
||||||
"i18next": "^23.15.1",
|
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
|
||||||
"i18next-http-backend": "^2.6.1",
|
|
||||||
"react-i18next": "^15.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { Access, accessFormType, getUsageByConfigType } from "@/domain/access";
|
||||||
Access,
|
|
||||||
accessFormType,
|
|
||||||
getUsageByConfigType,
|
|
||||||
LocalConfig,
|
|
||||||
SSHConfig,
|
|
||||||
} from "@/domain/access";
|
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
@ -20,7 +14,7 @@ import {
|
||||||
} from "../ui/form";
|
} from "../ui/form";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Textarea } from "../ui/textarea";
|
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { PbErrorData } from "@/domain/base";
|
import { PbErrorData } from "@/domain/base";
|
||||||
|
@ -39,30 +33,19 @@ const AccessLocalForm = ({
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, "access.form.name.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
configType: accessFormType,
|
configType: accessFormType,
|
||||||
|
|
||||||
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let config: LocalConfig = {
|
|
||||||
command: "sudo service nginx restart",
|
|
||||||
certPath: "/etc/nginx/ssl/certificate.crt",
|
|
||||||
keyPath: "/etc/nginx/ssl/private.key",
|
|
||||||
};
|
|
||||||
if (data) config = data.config as SSHConfig;
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: data?.id,
|
id: data?.id,
|
||||||
name: data?.name || '',
|
name: data?.name || "",
|
||||||
configType: "local",
|
configType: "local",
|
||||||
certPath: config.certPath,
|
|
||||||
keyPath: config.keyPath,
|
|
||||||
command: config.command,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -73,15 +56,11 @@ const AccessLocalForm = ({
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
usage: getUsageByConfigType(data.configType),
|
usage: getUsageByConfigType(data.configType),
|
||||||
|
|
||||||
config: {
|
config: {},
|
||||||
command: data.command,
|
|
||||||
certPath: data.certPath,
|
|
||||||
keyPath: data.keyPath,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
req.id = op == "copy" ? "" : req.id;
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
|
@ -128,9 +107,12 @@ const AccessLocalForm = ({
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('name')}</FormLabel>
|
<FormLabel>{t("name")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.name.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.name.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -143,7 +125,7 @@ const AccessLocalForm = ({
|
||||||
name="id"
|
name="id"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -158,7 +140,7 @@ const AccessLocalForm = ({
|
||||||
name="configType"
|
name="configType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -168,55 +150,10 @@ const AccessLocalForm = ({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="certPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="keyPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="command"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">{t('save')}</Button>
|
<Button type="submit">{t("save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
} from "../ui/form";
|
} from "../ui/form";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Textarea } from "../ui/textarea";
|
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { PbErrorData } from "@/domain/base";
|
import { PbErrorData } from "@/domain/base";
|
||||||
|
@ -66,7 +65,10 @@ const AccessSSHForm = ({
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, "access.form.name.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
configType: accessFormType,
|
configType: accessFormType,
|
||||||
host: z.string().refine(
|
host: z.string().refine(
|
||||||
(str) => {
|
(str) => {
|
||||||
|
@ -77,16 +79,23 @@ const AccessSSHForm = ({
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
group: z.string().optional(),
|
group: z.string().optional(),
|
||||||
port: z.string().min(1, 'access.form.ssh.port.not.empty').max(5, t('zod.rule.string.max', { max: 5 })),
|
port: z
|
||||||
username: z.string().min(1, 'username.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
.string()
|
||||||
password: z.string().min(0, 'password.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
.min(1, "access.form.ssh.port.not.empty")
|
||||||
key: z.string().min(0, 'access.form.ssh.key.not.empty').max(20480, t('zod.rule.string.max', { max: 20480 })),
|
.max(5, t("zod.rule.string.max", { max: 5 })),
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(1, "username.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(0, "password.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
|
key: z
|
||||||
|
.string()
|
||||||
|
.min(0, "access.form.ssh.key.not.empty")
|
||||||
|
.max(20480, t("zod.rule.string.max", { max: 20480 })),
|
||||||
keyFile: z.any().optional(),
|
keyFile: z.any().optional(),
|
||||||
|
|
||||||
preCommand: z.string().min(0).max(2048, t('zod.rule.string.max', { max: 2048 })).optional(),
|
|
||||||
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let config: SSHConfig = {
|
let config: SSHConfig = {
|
||||||
|
@ -96,10 +105,6 @@ const AccessSSHForm = ({
|
||||||
password: "",
|
password: "",
|
||||||
key: "",
|
key: "",
|
||||||
keyFile: "",
|
keyFile: "",
|
||||||
preCommand: "",
|
|
||||||
command: "sudo service nginx restart",
|
|
||||||
certPath: "/etc/nginx/ssl/certificate.crt",
|
|
||||||
keyPath: "/etc/nginx/ssl/private.key",
|
|
||||||
};
|
};
|
||||||
if (data) config = data.config as SSHConfig;
|
if (data) config = data.config as SSHConfig;
|
||||||
|
|
||||||
|
@ -107,7 +112,7 @@ const AccessSSHForm = ({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: data?.id,
|
id: data?.id,
|
||||||
name: data?.name || '',
|
name: data?.name || "",
|
||||||
configType: "ssh",
|
configType: "ssh",
|
||||||
group: data?.group,
|
group: data?.group,
|
||||||
host: config.host,
|
host: config.host,
|
||||||
|
@ -116,10 +121,6 @@ const AccessSSHForm = ({
|
||||||
password: config.password,
|
password: config.password,
|
||||||
key: config.key,
|
key: config.key,
|
||||||
keyFile: config.keyFile,
|
keyFile: config.keyFile,
|
||||||
certPath: config.certPath,
|
|
||||||
keyPath: config.keyPath,
|
|
||||||
command: config.command,
|
|
||||||
preCommand: config.preCommand,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -139,15 +140,11 @@ const AccessSSHForm = ({
|
||||||
username: data.username,
|
username: data.username,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
key: data.key,
|
key: data.key,
|
||||||
command: data.command,
|
|
||||||
preCommand: data.preCommand,
|
|
||||||
certPath: data.certPath,
|
|
||||||
keyPath: data.keyPath,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
req.id = op == "copy" ? "" : req.id;
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
|
@ -228,9 +225,12 @@ const AccessSSHForm = ({
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('name')}</FormLabel>
|
<FormLabel>{t("name")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.name.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.name.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -244,12 +244,12 @@ const AccessSSHForm = ({
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="w-full flex justify-between">
|
<FormLabel className="w-full flex justify-between">
|
||||||
<div>{t('access.form.ssh.group.label')}</div>
|
<div>{t("access.form.ssh.group.label")}</div>
|
||||||
<AccessGroupEdit
|
<AccessGroupEdit
|
||||||
trigger={
|
trigger={
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
{t('add')}
|
{t("add")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -264,7 +264,9 @@ const AccessSSHForm = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('access.group.not.empty')} />
|
<SelectValue
|
||||||
|
placeholder={t("access.group.not.empty")}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="emptyId">
|
<SelectItem value="emptyId">
|
||||||
|
@ -304,7 +306,7 @@ const AccessSSHForm = ({
|
||||||
name="id"
|
name="id"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -319,7 +321,7 @@ const AccessSSHForm = ({
|
||||||
name="configType"
|
name="configType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -334,9 +336,12 @@ const AccessSSHForm = ({
|
||||||
name="host"
|
name="host"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="grow">
|
<FormItem className="grow">
|
||||||
<FormLabel>{t('access.form.ssh.host')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.host")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.ssh.host.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.ssh.host.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -349,10 +354,10 @@ const AccessSSHForm = ({
|
||||||
name="port"
|
name="port"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('access.form.ssh.port')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.port")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('access.form.ssh.port.not.empty')}
|
placeholder={t("access.form.ssh.port.not.empty")}
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
|
@ -369,9 +374,9 @@ const AccessSSHForm = ({
|
||||||
name="username"
|
name="username"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('username')}</FormLabel>
|
<FormLabel>{t("username")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('username.not.empty')} {...field} />
|
<Input placeholder={t("username.not.empty")} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -384,10 +389,10 @@ const AccessSSHForm = ({
|
||||||
name="password"
|
name="password"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('password')}</FormLabel>
|
<FormLabel>{t("password")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('password.not.empty')}
|
placeholder={t("password.not.empty")}
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
|
@ -403,9 +408,12 @@ const AccessSSHForm = ({
|
||||||
name="key"
|
name="key"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden>
|
<FormItem hidden>
|
||||||
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.ssh.key.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.ssh.key.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -418,7 +426,7 @@ const AccessSSHForm = ({
|
||||||
name="keyFile"
|
name="keyFile"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
@ -428,10 +436,12 @@ const AccessSSHForm = ({
|
||||||
className="w-48"
|
className="w-48"
|
||||||
onClick={handleSelectFileClick}
|
onClick={handleSelectFileClick}
|
||||||
>
|
>
|
||||||
{fileName ? fileName : t('access.form.ssh.key.file.not.empty')}
|
{fileName
|
||||||
|
? fileName
|
||||||
|
: t("access.form.ssh.key.file.not.empty")}
|
||||||
</Button>
|
</Button>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('access.form.ssh.key.not.empty')}
|
placeholder={t("access.form.ssh.key.not.empty")}
|
||||||
{...field}
|
{...field}
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
|
@ -447,70 +457,10 @@ const AccessSSHForm = ({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="certPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="keyPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="preCommand"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.pre.command')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder={t('access.form.ssh.pre.command.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="command"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">{t('save')}</Button>
|
<Button type="submit">{t("save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -0,0 +1,689 @@
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { EditIcon, Plus, Trash2 } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DeployConfig,
|
||||||
|
KVType,
|
||||||
|
targetTypeKeys,
|
||||||
|
targetTypeMap,
|
||||||
|
} from "@/domain/domain";
|
||||||
|
import Show from "../Show";
|
||||||
|
import { Alert, AlertDescription } from "../ui/alert";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { useConfig } from "@/providers/config";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../ui/select";
|
||||||
|
import { accessTypeMap } from "@/domain/access";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AccessEdit } from "./AccessEdit";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import KVList from "./KVList";
|
||||||
|
import { produce } from "immer";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type DeployEditContextProps = {
|
||||||
|
deploy: DeployConfig;
|
||||||
|
error: Record<string, string>;
|
||||||
|
setDeploy: (deploy: DeployConfig) => void;
|
||||||
|
setError: (error: Record<string, string>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeployEditContext = createContext<DeployEditContextProps>(
|
||||||
|
{} as DeployEditContextProps
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useDeployEditContext = () => {
|
||||||
|
return useContext(DeployEditContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeployListProps = {
|
||||||
|
deploys: DeployConfig[];
|
||||||
|
onChange: (deploys: DeployConfig[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeployList = ({ deploys, onChange }: DeployListProps) => {
|
||||||
|
const [list, setList] = useState<DeployConfig[]>([]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setList(deploys);
|
||||||
|
}, [deploys]);
|
||||||
|
|
||||||
|
const handleAdd = (deploy: DeployConfig) => {
|
||||||
|
deploy.id = nanoid();
|
||||||
|
|
||||||
|
const newList = [...list, deploy];
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
|
||||||
|
onChange(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
const newList = list.filter((item) => item.id !== id);
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
|
||||||
|
onChange(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (deploy: DeployConfig) => {
|
||||||
|
const newList = list.map((item) => {
|
||||||
|
if (item.id === deploy.id) {
|
||||||
|
return { ...deploy };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
|
||||||
|
onChange(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show
|
||||||
|
when={list.length > 0}
|
||||||
|
fallback={
|
||||||
|
<Alert className="w-full border dark:border-stone-400">
|
||||||
|
<AlertDescription className="flex flex-col items-center">
|
||||||
|
<div>{t("deployment.not.added")}</div>
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<DeployEditDialog
|
||||||
|
onSave={(config: DeployConfig) => {
|
||||||
|
handleAdd(config);
|
||||||
|
}}
|
||||||
|
trigger={<Button size={"sm"}>{t("add")}</Button>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex justify-end py-2 border-b dark:border-stone-400">
|
||||||
|
<DeployEditDialog
|
||||||
|
trigger={<Button size={"sm"}>{t("add")}</Button>}
|
||||||
|
onSave={(config: DeployConfig) => {
|
||||||
|
handleAdd(config);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full md:w-[35em] rounded mt-5 border dark:border-stone-400">
|
||||||
|
<div className="">
|
||||||
|
{list.map((item) => (
|
||||||
|
<DeployItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onDelete={() => {
|
||||||
|
handleDelete(item.id ?? "");
|
||||||
|
}}
|
||||||
|
onSave={(deploy: DeployConfig) => {
|
||||||
|
handleSave(deploy);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeployItemProps = {
|
||||||
|
item: DeployConfig;
|
||||||
|
onDelete: () => void;
|
||||||
|
onSave: (deploy: DeployConfig) => void;
|
||||||
|
};
|
||||||
|
const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => {
|
||||||
|
const {
|
||||||
|
config: { accesses },
|
||||||
|
} = useConfig();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const access = accesses.find((access) => access.id === item.access);
|
||||||
|
const getImg = () => {
|
||||||
|
if (!access) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessType = accessTypeMap.get(access.configType);
|
||||||
|
|
||||||
|
if (accessType) {
|
||||||
|
return accessType[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeName = () => {
|
||||||
|
if (!access) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessType = targetTypeMap.get(item.type);
|
||||||
|
|
||||||
|
if (accessType) {
|
||||||
|
return t(accessType[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between text-sm p-3 items-center text-stone-700">
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<div>
|
||||||
|
<img src={getImg()} className="w-9"></img>
|
||||||
|
</div>
|
||||||
|
<div className="text-stone-600 flex-col flex space-y-0">
|
||||||
|
<div>{getTypeName()}</div>
|
||||||
|
<div>{access?.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<DeployEditDialog
|
||||||
|
trigger={<EditIcon size={16} className="cursor-pointer" />}
|
||||||
|
deployConfig={item}
|
||||||
|
onSave={(deploy: DeployConfig) => {
|
||||||
|
onSave(deploy);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trash2
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeployEditDialogProps = {
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
deployConfig?: DeployConfig;
|
||||||
|
onSave: (deploy: DeployConfig) => void;
|
||||||
|
};
|
||||||
|
const DeployEditDialog = ({
|
||||||
|
trigger,
|
||||||
|
deployConfig,
|
||||||
|
onSave,
|
||||||
|
}: DeployEditDialogProps) => {
|
||||||
|
const {
|
||||||
|
config: { accesses },
|
||||||
|
} = useConfig();
|
||||||
|
|
||||||
|
const [deployType, setDeployType] = useState<TargetType>();
|
||||||
|
|
||||||
|
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
|
||||||
|
access: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (deployConfig) {
|
||||||
|
setLocDeployConfig({ ...deployConfig });
|
||||||
|
} else {
|
||||||
|
setLocDeployConfig({
|
||||||
|
access: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [deployConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const temp = locDeployConfig.type.split("-");
|
||||||
|
console.log(temp);
|
||||||
|
let t;
|
||||||
|
if (temp && temp.length > 1) {
|
||||||
|
t = temp[1];
|
||||||
|
} else {
|
||||||
|
t = locDeployConfig.type;
|
||||||
|
}
|
||||||
|
setDeployType(t as TargetType);
|
||||||
|
setError({});
|
||||||
|
}, [locDeployConfig.type]);
|
||||||
|
|
||||||
|
const setDeploy = useCallback(
|
||||||
|
(deploy: DeployConfig) => {
|
||||||
|
if (deploy.type !== locDeployConfig.type) {
|
||||||
|
setLocDeployConfig({ ...deploy, access: "", config: {} });
|
||||||
|
} else {
|
||||||
|
setLocDeployConfig({ ...deploy });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[locDeployConfig.type]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const targetAccesses = accesses.filter((item) => {
|
||||||
|
if (item.usage == "apply") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locDeployConfig.type == "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const types = locDeployConfig.type.split("-");
|
||||||
|
return item.configType === types[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
// 验证数据
|
||||||
|
// 保存数据
|
||||||
|
// 清理数据
|
||||||
|
// 关闭弹框
|
||||||
|
const newError = { ...error };
|
||||||
|
if (locDeployConfig.type === "") {
|
||||||
|
newError.type = t("domain.management.edit.access.not.empty.message");
|
||||||
|
} else {
|
||||||
|
newError.type = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locDeployConfig.access === "") {
|
||||||
|
newError.access = t("domain.management.edit.access.not.empty.message");
|
||||||
|
} else {
|
||||||
|
newError.access = "";
|
||||||
|
}
|
||||||
|
setError(newError);
|
||||||
|
|
||||||
|
for (const key in newError) {
|
||||||
|
if (newError[key] !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(locDeployConfig);
|
||||||
|
|
||||||
|
setLocDeployConfig({
|
||||||
|
access: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setError({});
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeployEditContext.Provider
|
||||||
|
value={{
|
||||||
|
deploy: locDeployConfig,
|
||||||
|
setDeploy: setDeploy,
|
||||||
|
error: error,
|
||||||
|
setError: setError,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger>{trigger}</DialogTrigger>
|
||||||
|
<DialogContent className="dark:text-stone-200">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("deployment")}</DialogTitle>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{/* 授权类型 */}
|
||||||
|
<div>
|
||||||
|
<Label>{t("deployment.access.type")}</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={locDeployConfig.type}
|
||||||
|
onValueChange={(val: string) => {
|
||||||
|
setDeploy({ ...locDeployConfig, type: val });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-2">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.access.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>
|
||||||
|
{t("domain.management.edit.access.label")}
|
||||||
|
</SelectLabel>
|
||||||
|
{targetTypeKeys.map((item) => (
|
||||||
|
<SelectItem key={item} value={item}>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
className="w-6"
|
||||||
|
src={targetTypeMap.get(item)?.[1]}
|
||||||
|
/>
|
||||||
|
<div>{t(targetTypeMap.get(item)?.[0] ?? "")}</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="text-red-500 text-sm mt-1">{error.type}</div>
|
||||||
|
</div>
|
||||||
|
{/* 授权 */}
|
||||||
|
<div>
|
||||||
|
<Label className="flex justify-between">
|
||||||
|
<div>{t("deployment.access.config")}</div>
|
||||||
|
<AccessEdit
|
||||||
|
trigger={
|
||||||
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
|
<Plus size={14} />
|
||||||
|
{t("add")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
op="add"
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={locDeployConfig.access}
|
||||||
|
onValueChange={(val: string) => {
|
||||||
|
setDeploy({ ...locDeployConfig, access: val });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-2">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.access.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>
|
||||||
|
{t("domain.management.edit.access.label")}
|
||||||
|
</SelectLabel>
|
||||||
|
{targetAccesses.map((item) => (
|
||||||
|
<SelectItem key={item.id} value={item.id}>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
className="w-6"
|
||||||
|
src={accessTypeMap.get(item.configType)?.[1]}
|
||||||
|
/>
|
||||||
|
<div>{item.name}</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="text-red-500 text-sm mt-1">{error.access}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeployEdit type={deployType!} />
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</DeployEditContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TargetType = "ssh" | "cdn" | "webhook" | "local" | "oss" | "dcdn";
|
||||||
|
|
||||||
|
type DeployEditProps = {
|
||||||
|
type: TargetType;
|
||||||
|
};
|
||||||
|
const DeployEdit = ({ type }: DeployEditProps) => {
|
||||||
|
const getDeploy = () => {
|
||||||
|
switch (type) {
|
||||||
|
case "ssh":
|
||||||
|
return <DeploySSH />;
|
||||||
|
case "local":
|
||||||
|
return <DeploySSH />;
|
||||||
|
case "cdn":
|
||||||
|
return <DeployCDN />;
|
||||||
|
case "dcdn":
|
||||||
|
return <DeployCDN />;
|
||||||
|
case "oss":
|
||||||
|
return <DeployCDN />;
|
||||||
|
case "webhook":
|
||||||
|
return <DeployWebhook />;
|
||||||
|
default:
|
||||||
|
return <DeployCDN />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return getDeploy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeploySSH = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setError } = useDeployEditContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { deploy: data, setDeploy } = useDeployEditContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data.id) {
|
||||||
|
setDeploy({
|
||||||
|
...data,
|
||||||
|
config: {
|
||||||
|
certPath: "/etc/nginx/ssl/nginx.crt",
|
||||||
|
keyPath: "/etc/nginx/ssl/nginx.key",
|
||||||
|
preCommand: "",
|
||||||
|
command: "sudo service nginx reload",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div>
|
||||||
|
<Label>{t("access.form.ssh.cert.path")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("access.form.ssh.cert.path")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.certPath}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
if (!draft.config) {
|
||||||
|
draft.config = {};
|
||||||
|
}
|
||||||
|
draft.config.certPath = e.target.value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>{t("access.form.ssh.key.path")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("access.form.ssh.key.path")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.keyPath}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
if (!draft.config) {
|
||||||
|
draft.config = {};
|
||||||
|
}
|
||||||
|
draft.config.keyPath = e.target.value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("access.form.ssh.pre.command")}</Label>
|
||||||
|
<Textarea
|
||||||
|
className="mt-1"
|
||||||
|
value={data?.config?.preCommand}
|
||||||
|
placeholder={t("access.form.ssh.pre.command.not.empty")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
if (!draft.config) {
|
||||||
|
draft.config = {};
|
||||||
|
}
|
||||||
|
draft.config.preCommand = e.target.value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("access.form.ssh.command")}</Label>
|
||||||
|
<Textarea
|
||||||
|
className="mt-1"
|
||||||
|
value={data?.config?.command}
|
||||||
|
placeholder={t("access.form.ssh.command.not.empty")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
if (!draft.config) {
|
||||||
|
draft.config = {};
|
||||||
|
}
|
||||||
|
draft.config.command = e.target.value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeployCDN = () => {
|
||||||
|
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resp = domainSchema.safeParse(data.config?.domain);
|
||||||
|
if (!resp.success) {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: JSON.parse(resp.error.message)[0].message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const domainSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
|
||||||
|
message: t("domain.not.empty.verify.message"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div>
|
||||||
|
<Label>{t("deployment.access.cdn.deploy.to.domain")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("deployment.access.cdn.deploy.to.domain")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.domain}
|
||||||
|
onChange={(e) => {
|
||||||
|
const temp = e.target.value;
|
||||||
|
|
||||||
|
const resp = domainSchema.safeParse(temp);
|
||||||
|
if (!resp.success) {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: JSON.parse(resp.error.message)[0].message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
if (!draft.config) {
|
||||||
|
draft.config = {};
|
||||||
|
}
|
||||||
|
draft.config.domain = temp;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeployWebhook = () => {
|
||||||
|
const { deploy: data, setDeploy } = useDeployEditContext();
|
||||||
|
|
||||||
|
const { setError } = useDeployEditContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<KVList
|
||||||
|
variables={data?.config?.variables}
|
||||||
|
onValueChange={(variables: KVType[]) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
if (!draft.config) {
|
||||||
|
draft.config = {};
|
||||||
|
}
|
||||||
|
draft.config.variables = variables;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeployList;
|
|
@ -0,0 +1,252 @@
|
||||||
|
import { KVType } from "@/domain/domain";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { Edit, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Show from "../Show";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
import { produce } from "immer";
|
||||||
|
|
||||||
|
type KVListProps = {
|
||||||
|
variables?: KVType[];
|
||||||
|
onValueChange?: (variables: KVType[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KVList = ({ variables, onValueChange }: KVListProps) => {
|
||||||
|
const [locVariables, setLocVariables] = useState<KVType[]>([]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (variables) {
|
||||||
|
setLocVariables(variables);
|
||||||
|
}
|
||||||
|
}, [variables]);
|
||||||
|
|
||||||
|
const handleAddClick = (variable: KVType) => {
|
||||||
|
// 查看是否存在key,存在则更新,不存在则添加
|
||||||
|
const index = locVariables.findIndex((item) => {
|
||||||
|
return item.key === variable.key;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newList = produce(locVariables, (draft) => {
|
||||||
|
if (index === -1) {
|
||||||
|
draft.push(variable);
|
||||||
|
} else {
|
||||||
|
draft[index] = variable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setLocVariables(newList);
|
||||||
|
|
||||||
|
onValueChange?.(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (index: number) => {
|
||||||
|
const newList = [...locVariables];
|
||||||
|
newList.splice(index, 1);
|
||||||
|
setLocVariables(newList);
|
||||||
|
|
||||||
|
onValueChange?.(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (index: number, variable: KVType) => {
|
||||||
|
const newList = [...locVariables];
|
||||||
|
newList[index] = variable;
|
||||||
|
setLocVariables(newList);
|
||||||
|
|
||||||
|
onValueChange?.(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between dark:text-stone-200">
|
||||||
|
<Label>{t("variable")}</Label>
|
||||||
|
<Show when={!!locVariables?.length}>
|
||||||
|
<KVEdit
|
||||||
|
variable={{
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<div className="flex items-center text-primary">
|
||||||
|
<Plus size={16} className="cursor-pointer " />
|
||||||
|
|
||||||
|
<div className="text-sm ">{t("add")}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onSave={(variable) => {
|
||||||
|
handleAddClick(variable);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={!!locVariables?.length}
|
||||||
|
fallback={
|
||||||
|
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{t("variable.not.added")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<KVEdit
|
||||||
|
trigger={
|
||||||
|
<div className="flex items-center text-primary">
|
||||||
|
<Plus size={16} className="cursor-pointer " />
|
||||||
|
|
||||||
|
<div className="text-sm ">{t("add")}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
variable={{
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
}}
|
||||||
|
onSave={(variable) => {
|
||||||
|
handleAddClick(variable);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="border p-3 rounded-md text-stone-700 text-sm dark:text-stone-200">
|
||||||
|
{locVariables?.map((item, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
{item.key}={item.value}
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<KVEdit
|
||||||
|
trigger={<Edit size={16} className="cursor-pointer" />}
|
||||||
|
variable={item}
|
||||||
|
onSave={(variable) => {
|
||||||
|
handleEditClick(index, variable);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trash2
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteClick(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type KVEditProps = {
|
||||||
|
variable?: KVType;
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
onSave: (variable: KVType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
|
||||||
|
const [locVariable, setLocVariable] = useState<KVType>({
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (variable) setLocVariable(variable!);
|
||||||
|
}, [variable]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [err, setErr] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
if (!locVariable.key) {
|
||||||
|
setErr({
|
||||||
|
key: t("variable.name.required"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locVariable.value) {
|
||||||
|
setErr({
|
||||||
|
value: t("variable.value.required"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave?.(locVariable);
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
setErr({});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={() => {
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger>{trigger}</DialogTrigger>
|
||||||
|
<DialogContent className="dark:text-stone-200">
|
||||||
|
<DialogHeader className="flex flex-col">
|
||||||
|
<DialogTitle>{t("variable")}</DialogTitle>
|
||||||
|
|
||||||
|
<div className="pt-5 flex flex-col items-start">
|
||||||
|
<Label>{t("variable.name")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("variable.name.placeholder")}
|
||||||
|
value={locVariable?.key}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLocVariable({ ...locVariable, key: e.target.value });
|
||||||
|
}}
|
||||||
|
className="w-full mt-1"
|
||||||
|
/>
|
||||||
|
<div className="text-red-500 text-sm mt-1">{err?.key}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2 flex flex-col items-start">
|
||||||
|
<Label>{t("variable.value")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("variable.value.placeholder")}
|
||||||
|
value={locVariable?.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLocVariable({ ...locVariable, value: e.target.value });
|
||||||
|
}}
|
||||||
|
className="w-full mt-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="text-red-500 text-sm mt-1">{err?.value}</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KVList;
|
|
@ -20,13 +20,14 @@ import { Edit, Plus, Trash2 } from "lucide-react";
|
||||||
type StringListProps = {
|
type StringListProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
value: string;
|
value: string;
|
||||||
valueType?: "domain" | "ip";
|
valueType?: ValueType;
|
||||||
onValueChange: (value: string) => void;
|
onValueChange: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const titles: Record<string, string> = {
|
const titles: Record<string, string> = {
|
||||||
domain: "domain",
|
domain: "domain",
|
||||||
ip: "IP",
|
ip: "IP",
|
||||||
|
dns: "dns",
|
||||||
};
|
};
|
||||||
|
|
||||||
const StringList = ({
|
const StringList = ({
|
||||||
|
@ -100,7 +101,9 @@ const StringList = ({
|
||||||
when={list.length > 0}
|
when={list.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
|
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
|
||||||
<div className="text-muted-foreground">暂未添加域名</div>
|
<div className="text-muted-foreground">
|
||||||
|
{t("not.added.yet." + valueType)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<StringEdit
|
<StringEdit
|
||||||
value={""}
|
value={""}
|
||||||
|
@ -150,7 +153,7 @@ const StringList = ({
|
||||||
|
|
||||||
export default StringList;
|
export default StringList;
|
||||||
|
|
||||||
type ValueType = "domain" | "ip";
|
type ValueType = "domain" | "dns" | "host";
|
||||||
|
|
||||||
type StringEditProps = {
|
type StringEditProps = {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -186,7 +189,8 @@ const StringEdit = ({
|
||||||
|
|
||||||
const schedules: Record<ValueType, z.ZodString> = {
|
const schedules: Record<ValueType, z.ZodString> = {
|
||||||
domain: domainSchema,
|
domain: domainSchema,
|
||||||
ip: ipSchema,
|
dns: ipSchema,
|
||||||
|
host: ipSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSaveClick = useCallback(() => {
|
const onSaveClick = useCallback(() => {
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Breadcrumb = React.forwardRef<
|
||||||
|
HTMLElement,
|
||||||
|
React.ComponentPropsWithoutRef<"nav"> & {
|
||||||
|
separator?: React.ReactNode
|
||||||
|
}
|
||||||
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||||
|
Breadcrumb.displayName = "Breadcrumb"
|
||||||
|
|
||||||
|
const BreadcrumbList = React.forwardRef<
|
||||||
|
HTMLOListElement,
|
||||||
|
React.ComponentPropsWithoutRef<"ol">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ol
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbList.displayName = "BreadcrumbList"
|
||||||
|
|
||||||
|
const BreadcrumbItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentPropsWithoutRef<"li">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<li
|
||||||
|
ref={ref}
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||||
|
|
||||||
|
const BreadcrumbLink = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
React.ComponentPropsWithoutRef<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
>(({ asChild, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
className={cn("transition-colors hover:text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||||
|
|
||||||
|
const BreadcrumbPage = React.forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
React.ComponentPropsWithoutRef<"span">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("font-normal text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||||
|
|
||||||
|
const BreadcrumbSeparator = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) => (
|
||||||
|
<li
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||||
|
|
||||||
|
const BreadcrumbEllipsis = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) => (
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
|
@ -91,23 +91,15 @@ export type GodaddyConfig = {
|
||||||
apiSecret: string;
|
apiSecret: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LocalConfig = {
|
export type LocalConfig = Record<string, string>;
|
||||||
command: string;
|
|
||||||
certPath: string;
|
|
||||||
keyPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSHConfig = {
|
export type SSHConfig = {
|
||||||
host: string;
|
host: string;
|
||||||
port: string;
|
port: string;
|
||||||
preCommand?: string;
|
|
||||||
command: string;
|
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
keyFile?: string;
|
keyFile?: string;
|
||||||
certPath: string;
|
|
||||||
keyPath: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebhookConfig = {
|
export type WebhookConfig = {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Deployment, Pahse } from "./deployment";
|
import { Deployment, Pahse } from "./deployment";
|
||||||
|
|
||||||
export type Domain = {
|
export type Domain = {
|
||||||
id: string;
|
id?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
crontab: string;
|
crontab: string;
|
||||||
access: string;
|
access: string;
|
||||||
targetAccess?: string;
|
targetAccess?: string;
|
||||||
targetType: string;
|
targetType?: string;
|
||||||
expiredAt?: string;
|
expiredAt?: string;
|
||||||
phase?: Pahse;
|
phase?: Pahse;
|
||||||
phaseSuccess?: boolean;
|
phaseSuccess?: boolean;
|
||||||
|
@ -31,10 +31,20 @@ export type Domain = {
|
||||||
deployConfig?: DeployConfig[];
|
deployConfig?: DeployConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type KVType = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type DeployConfig = {
|
export type DeployConfig = {
|
||||||
|
id?: string;
|
||||||
access: string;
|
access: string;
|
||||||
type: string;
|
type: string;
|
||||||
config?: Record<string, string>;
|
config?: {
|
||||||
|
[key: string]: string;
|
||||||
|
} & {
|
||||||
|
variables?: KVType[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApplyConfig = {
|
export type ApplyConfig = {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export const version = "Certimate v0.1.19";
|
export const version = "Certimate v0.2.0";
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
"username.not.empty": "Please enter username",
|
"username.not.empty": "Please enter username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"password.not.empty": "Please enter password",
|
"password.not.empty": "Please enter password",
|
||||||
|
"ip.not.empty.verify.message": "Please enter Ip",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"setting": "Settings",
|
"setting": "Settings",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"template": "Template",
|
"template": "Template",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"next": "Next",
|
||||||
"no.data": "No data available",
|
"no.data": "No data available",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"operation": "Operation",
|
"operation": "Operation",
|
||||||
|
@ -28,12 +30,15 @@
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
"dns": "Domain Name Server",
|
"dns": "Domain Name Server",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"timeout": "Time Out",
|
||||||
|
"not.added.yet.domain": "Domain not added yet.",
|
||||||
|
"not.added.yet.dns": "Nameserver not added yet.",
|
||||||
"create.time": "CreateTime",
|
"create.time": "CreateTime",
|
||||||
"update.time": "UpdateTime",
|
"update.time": "UpdateTime",
|
||||||
"created.in": "Created in",
|
"created.in": "Created in",
|
||||||
"updated.in": "Updated in",
|
"updated.in": "Updated in",
|
||||||
"basic.setting": "Basic Settings",
|
"apply.setting": "Apply Settings",
|
||||||
"advanced.setting": "Advanced Settings",
|
"deploy.setting": "Deploy Settings",
|
||||||
"operation.succeed": "Operation Successful",
|
"operation.succeed": "Operation Successful",
|
||||||
"save.succeed": "Save Successful",
|
"save.succeed": "Save Successful",
|
||||||
"save.failed": "Save Failed",
|
"save.failed": "Save Failed",
|
||||||
|
@ -84,7 +89,7 @@
|
||||||
"pagination.prev": "Previous",
|
"pagination.prev": "Previous",
|
||||||
"domain": "Domain",
|
"domain": "Domain",
|
||||||
"domain.add": "Add Domain",
|
"domain.add": "Add Domain",
|
||||||
"domain.edit":"Edit Domain",
|
"domain.edit": "Edit Domain",
|
||||||
"domain.delete": "Delete Domain",
|
"domain.delete": "Delete Domain",
|
||||||
"domain.not.empty.verify.message": "Please enter domain",
|
"domain.not.empty.verify.message": "Please enter domain",
|
||||||
"domain.management.name": "Domain List",
|
"domain.management.name": "Domain List",
|
||||||
|
@ -121,6 +126,10 @@
|
||||||
"domain.management.edit.variables.placeholder": "It can be used in SSH deployment, like:\nkey=val;\nkey2=val2;",
|
"domain.management.edit.variables.placeholder": "It can be used in SSH deployment, like:\nkey=val;\nkey2=val2;",
|
||||||
"domain.management.edit.dns.placeholder": "Custom domain name server, separates multiple entries with semicolon, like:\n8.8.8.8;\n8.8.4.4;",
|
"domain.management.edit.dns.placeholder": "Custom domain name server, separates multiple entries with semicolon, like:\n8.8.8.8;\n8.8.4.4;",
|
||||||
"domain.management.add.succeed.tips": "Domain added successfully",
|
"domain.management.add.succeed.tips": "Domain added successfully",
|
||||||
|
"domain.management.edit.timeout.placeholder": "Timeout (seconds)",
|
||||||
|
"domain.management.edit.deploy.error": "Please save applyment configuration first",
|
||||||
|
"domain.management.enabled.failed": "Enable failed",
|
||||||
|
"domain.management.enabled.without.deployments": "Failed to enable, no deployment configuration found",
|
||||||
"email.add": "Add Email",
|
"email.add": "Add Email",
|
||||||
"email.list": "Email List",
|
"email.list": "Email List",
|
||||||
"email.valid.message": "Please enter a valid email address",
|
"email.valid.message": "Please enter a valid email address",
|
||||||
|
@ -215,5 +224,20 @@
|
||||||
"access.form.ssh.pre.command.not.empty": "Command to be executed before deploying the certificate",
|
"access.form.ssh.pre.command.not.empty": "Command to be executed before deploying the certificate",
|
||||||
"access.form.ssh.command": "Command",
|
"access.form.ssh.command": "Command",
|
||||||
"access.form.ssh.command.not.empty": "Please enter command",
|
"access.form.ssh.command.not.empty": "Please enter command",
|
||||||
"access.form.ding.access.token.placeholder": "Signature for signed addition"
|
"access.form.ding.access.token.placeholder": "Signature for signed addition",
|
||||||
|
|
||||||
|
"variable": "Variable",
|
||||||
|
"variable.name": "Name",
|
||||||
|
"variable.value": "Value",
|
||||||
|
"variable.not.added": "Variable not added yet",
|
||||||
|
"variable.name.required": "Variable name cannot be empty",
|
||||||
|
"variable.value.required": "Variable value cannot be empty",
|
||||||
|
"variable.name.placeholder": "Variable name",
|
||||||
|
"variable.value.placeholder": "Variable value",
|
||||||
|
|
||||||
|
"deployment": "Deployment",
|
||||||
|
"deployment.not.added": "Deployment not added yet",
|
||||||
|
"deployment.access.type": "Access Type",
|
||||||
|
"deployment.access.config": "Access Configuration",
|
||||||
|
"deployment.access.cdn.deploy.to.domain": "Deploy to domain"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
"username.not.empty": "请输入用户名",
|
"username.not.empty": "请输入用户名",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"password.not.empty": "请输入密码",
|
"password.not.empty": "请输入密码",
|
||||||
|
"ip.not.empty.verify.message": "请输入 IP",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"logout": "退出登录",
|
"logout": "退出登录",
|
||||||
"setting": "设置",
|
"setting": "设置",
|
||||||
"account": "账户",
|
"account": "账户",
|
||||||
"template": "模版",
|
"template": "模版",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
|
"next": "下一步",
|
||||||
"no.data": "暂无数据",
|
"no.data": "暂无数据",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"operation": "操作",
|
"operation": "操作",
|
||||||
|
@ -28,12 +30,15 @@
|
||||||
"variables": "变量",
|
"variables": "变量",
|
||||||
"dns": "域名服务器",
|
"dns": "域名服务器",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
|
"timeout": "超时时间",
|
||||||
|
"not.added.yet.domain": "域名未添加",
|
||||||
|
"not.added.yet.dns": "域名服务器暂未添加",
|
||||||
"create.time": "创建时间",
|
"create.time": "创建时间",
|
||||||
"update.time": "更新时间",
|
"update.time": "更新时间",
|
||||||
"created.in": "创建于",
|
"created.in": "创建于",
|
||||||
"updated.in": "更新于",
|
"updated.in": "更新于",
|
||||||
"basic.setting": "基础设置",
|
"apply.setting": "申请设置",
|
||||||
"advanced.setting": "高级设置",
|
"deploy.setting": "部署设置",
|
||||||
"operation.succeed": "操作成功",
|
"operation.succeed": "操作成功",
|
||||||
"save.succeed": "保存成功",
|
"save.succeed": "保存成功",
|
||||||
"save.failed": "保存失败",
|
"save.failed": "保存失败",
|
||||||
|
@ -121,6 +126,10 @@
|
||||||
"domain.management.edit.variables.placeholder": "可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;",
|
"domain.management.edit.variables.placeholder": "可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;",
|
||||||
"domain.management.edit.dns.placeholder": "自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;",
|
"domain.management.edit.dns.placeholder": "自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;",
|
||||||
"domain.management.add.succeed.tips": "域名添加成功",
|
"domain.management.add.succeed.tips": "域名添加成功",
|
||||||
|
"domain.management.edit.timeout.placeholder": "超时时间(单位:秒)",
|
||||||
|
"domain.management.edit.deploy.error": "请先保存申请配置",
|
||||||
|
"domain.management.enabled.failed": "启用失败",
|
||||||
|
"domain.management.enabled.without.deployments": "启用失败,请先设置部署配置",
|
||||||
"email.add": "添加邮箱",
|
"email.add": "添加邮箱",
|
||||||
"email.list": "邮箱列表",
|
"email.list": "邮箱列表",
|
||||||
"email.valid.message": "请输入正确的邮箱地址",
|
"email.valid.message": "请输入正确的邮箱地址",
|
||||||
|
@ -215,5 +224,20 @@
|
||||||
"access.form.ssh.pre.command.not.empty": "在部署证书前执行的前置命令",
|
"access.form.ssh.pre.command.not.empty": "在部署证书前执行的前置命令",
|
||||||
"access.form.ssh.command": "Command",
|
"access.form.ssh.command": "Command",
|
||||||
"access.form.ssh.command.not.empty": "请输入要执行的命令",
|
"access.form.ssh.command.not.empty": "请输入要执行的命令",
|
||||||
"access.form.ding.access.token.placeholder": "加签的签名"
|
"access.form.ding.access.token.placeholder": "加签的签名",
|
||||||
|
|
||||||
|
"variable": "变量",
|
||||||
|
"variable.name": "变量名",
|
||||||
|
"variable.value": "值",
|
||||||
|
"variable.not.added": "尚未添加变量",
|
||||||
|
"variable.name.required": "变量名不能为空",
|
||||||
|
"variable.value.required": "变量值不能为空",
|
||||||
|
"variable.name.placeholder": "请输入变量名",
|
||||||
|
"variable.value.placeholder": "请输入变量值",
|
||||||
|
|
||||||
|
"deployment": "部署",
|
||||||
|
"deployment.not.added": "暂无部署配置,请添加后开始部署证书吧",
|
||||||
|
"deployment.access.type": "授权类型",
|
||||||
|
"deployment.access.config": "授权配置",
|
||||||
|
"deployment.access.cdn.deploy.to.domain": "部署到域名"
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,36 +23,43 @@ import {
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Domain, targetTypeKeys, targetTypeMap } from "@/domain/domain";
|
import { DeployConfig, Domain } from "@/domain/domain";
|
||||||
import { save, get } from "@/repository/domains";
|
import { save, get } from "@/repository/domains";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { PbErrorData } from "@/domain/base";
|
import { PbErrorData } from "@/domain/base";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { AccessEdit } from "@/components/certimate/AccessEdit";
|
import { AccessEdit } from "@/components/certimate/AccessEdit";
|
||||||
import { accessTypeMap } from "@/domain/access";
|
import { accessTypeMap } from "@/domain/access";
|
||||||
import EmailsEdit from "@/components/certimate/EmailsEdit";
|
import EmailsEdit from "@/components/certimate/EmailsEdit";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EmailsSetting } from "@/domain/settings";
|
import { EmailsSetting } from "@/domain/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import StringList from "@/components/certimate/StringList";
|
import StringList from "@/components/certimate/StringList";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import DeployList from "@/components/certimate/DeployList";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = () => {
|
||||||
const {
|
const {
|
||||||
config: { accesses, emails, accessGroups },
|
config: { accesses, emails },
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
const [domain, setDomain] = useState<Domain>();
|
const [domain, setDomain] = useState<Domain>({} as Domain);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [tab, setTab] = useState<"base" | "advance">("base");
|
const [tab, setTab] = useState<"apply" | "deploy">("apply");
|
||||||
|
|
||||||
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Parsing query parameters
|
// Parsing query parameters
|
||||||
|
@ -62,7 +69,6 @@ const Edit = () => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const data = await get(id);
|
const data = await get(id);
|
||||||
setDomain(data);
|
setDomain(data);
|
||||||
setTargetType(data.targetType);
|
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
@ -77,13 +83,8 @@ const Edit = () => {
|
||||||
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
||||||
message: "domain.management.edit.dns.access.not.empty.message",
|
message: "domain.management.edit.dns.access.not.empty.message",
|
||||||
}),
|
}),
|
||||||
targetAccess: z.string().optional(),
|
|
||||||
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
|
|
||||||
message: "domain.management.edit.target.type.not.empty.message",
|
|
||||||
}),
|
|
||||||
variables: z.string().optional(),
|
|
||||||
group: z.string().optional(),
|
|
||||||
nameservers: z.string().optional(),
|
nameservers: z.string().optional(),
|
||||||
|
timeout: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
@ -93,11 +94,8 @@ const Edit = () => {
|
||||||
domain: "",
|
domain: "",
|
||||||
email: "",
|
email: "",
|
||||||
access: "",
|
access: "",
|
||||||
targetAccess: "",
|
|
||||||
targetType: "",
|
|
||||||
variables: "",
|
|
||||||
group: "",
|
|
||||||
nameservers: "",
|
nameservers: "",
|
||||||
|
timeout: 60,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -106,64 +104,35 @@ const Edit = () => {
|
||||||
form.reset({
|
form.reset({
|
||||||
id: domain.id,
|
id: domain.id,
|
||||||
domain: domain.domain,
|
domain: domain.domain,
|
||||||
email: domain.email,
|
email: domain.applyConfig?.email,
|
||||||
access: domain.access,
|
access: domain.applyConfig?.access,
|
||||||
targetAccess: domain.targetAccess,
|
|
||||||
targetType: domain.targetType,
|
nameservers: domain.applyConfig?.nameservers,
|
||||||
variables: domain.variables,
|
timeout: domain.applyConfig?.timeout,
|
||||||
group: domain.group,
|
|
||||||
nameservers: domain.nameservers,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [domain, form]);
|
}, [domain, form]);
|
||||||
|
|
||||||
const targetAccesses = accesses.filter((item) => {
|
|
||||||
if (item.usage == "apply") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == "") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const types = targetType.split("-");
|
|
||||||
return item.configType === types[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||||
const group = data.group == "emptyId" ? "" : data.group;
|
console.log(data);
|
||||||
const targetAccess =
|
|
||||||
data.targetAccess === "emptyId" ? "" : data.targetAccess;
|
|
||||||
if (group == "" && targetAccess == "") {
|
|
||||||
form.setError("group", {
|
|
||||||
type: "manual",
|
|
||||||
message: "domain.management.edit.target.access.verify.msg",
|
|
||||||
});
|
|
||||||
form.setError("targetAccess", {
|
|
||||||
type: "manual",
|
|
||||||
message: "domain.management.edit.target.access.verify.msg",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const req: Domain = {
|
const req: Domain = {
|
||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
crontab: "0 0 * * *",
|
crontab: "0 0 * * *",
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
access: data.access,
|
access: data.access,
|
||||||
group: group,
|
applyConfig: {
|
||||||
targetAccess: targetAccess,
|
email: data.email ?? "",
|
||||||
targetType: data.targetType,
|
access: data.access,
|
||||||
variables: data.variables,
|
nameservers: data.nameservers,
|
||||||
nameservers: data.nameservers,
|
timeout: data.timeout,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await save(req);
|
const resp = await save(req);
|
||||||
let description = t("domain.management.edit.succeed.tips");
|
let description = t("domain.management.edit.succeed.tips");
|
||||||
if (req.id == "") {
|
if (req.id == "") {
|
||||||
description = t("domain.management.add.succeed.tips");
|
description = t("domain.management.add.succeed.tips");
|
||||||
|
@ -173,7 +142,44 @@ const Edit = () => {
|
||||||
title: t("succeed"),
|
title: t("succeed"),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
navigate("/domains");
|
|
||||||
|
if (!domain?.id) setTab("deploy");
|
||||||
|
setDomain({ ...resp });
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as ClientResponseError;
|
||||||
|
|
||||||
|
Object.entries(err.response.data as PbErrorData).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
form.setError(key as keyof z.infer<typeof formSchema>, {
|
||||||
|
type: "manual",
|
||||||
|
message: value.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handelOnDeployListChange = async (list: DeployConfig[]) => {
|
||||||
|
const req = {
|
||||||
|
...domain,
|
||||||
|
deployConfig: list,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const resp = await save(req);
|
||||||
|
let description = t("domain.management.edit.succeed.tips");
|
||||||
|
if (req.id == "") {
|
||||||
|
description = t("domain.management.add.succeed.tips");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("succeed"),
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!domain?.id) setTab("deploy");
|
||||||
|
setDomain({ ...resp });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = e as ClientResponseError;
|
const err = e as ClientResponseError;
|
||||||
|
|
||||||
|
@ -195,403 +201,279 @@ const Edit = () => {
|
||||||
<div className="">
|
<div className="">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<div className=" h-5 text-muted-foreground">
|
<div className=" h-5 text-muted-foreground">
|
||||||
{domain?.id ? t("domain.edit") : t("domain.add")}
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink href="#/domains">
|
||||||
|
{t("domain.management.name")}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>
|
||||||
|
{domain?.id ? t("domain.edit") : t("domain.add")}
|
||||||
|
</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
|
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
|
||||||
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex">
|
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex md:mt-5">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer text-right",
|
"cursor-pointer text-right",
|
||||||
tab === "base" ? "text-primary" : ""
|
tab === "apply" ? "text-primary" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTab("base");
|
setTab("apply");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("basic.setting")}
|
{t("apply.setting")}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer text-right",
|
"cursor-pointer text-right",
|
||||||
tab === "advance" ? "text-primary" : ""
|
tab === "deploy" ? "text-primary" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTab("advance");
|
if (!domain?.id) {
|
||||||
|
toast({
|
||||||
|
title: t("domain.management.edit.deploy.error"),
|
||||||
|
description: t("domain.management.edit.deploy.error"),
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTab("deploy");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("advanced.setting")}
|
{t("deploy.setting")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full md:w-[35em] p-5 rounded mt-3 md:mt-0",
|
||||||
|
tab == "deploy" && "hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-8 dark:text-stone-200"
|
||||||
|
>
|
||||||
|
{/* 域名 */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="domain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<>
|
||||||
|
<StringList
|
||||||
|
value={field.value}
|
||||||
|
valueType="domain"
|
||||||
|
onValueChange={(domain: string) => {
|
||||||
|
form.setValue("domain", domain);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
<div className="w-full md:w-[35em] bg-gray-100 dark:bg-gray-900 p-5 rounded mt-3 md:mt-0">
|
<FormMessage />
|
||||||
<Form {...form}>
|
</FormItem>
|
||||||
<form
|
)}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
/>
|
||||||
className="space-y-8 dark:text-stone-200"
|
{/* 邮箱 */}
|
||||||
>
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="email"
|
||||||
name="domain"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem hidden={tab != "base"}>
|
<FormLabel className="flex w-full justify-between">
|
||||||
<>
|
<div>
|
||||||
<StringList
|
{t("email") +
|
||||||
value={field.value}
|
t("domain.management.edit.email.description")}
|
||||||
valueType="domain"
|
</div>
|
||||||
onValueChange={(domain: string) => {
|
<EmailsEdit
|
||||||
form.setValue("domain", domain);
|
trigger={
|
||||||
}}
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
/>
|
<Plus size={14} />
|
||||||
</>
|
{t("add")}
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem hidden={tab != "base"}>
|
|
||||||
<FormLabel className="flex w-full justify-between">
|
|
||||||
<div>
|
|
||||||
{t("email") +
|
|
||||||
t("domain.management.edit.email.description")}
|
|
||||||
</div>
|
|
||||||
<EmailsEdit
|
|
||||||
trigger={
|
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
|
||||||
<Plus size={14} />
|
|
||||||
{t("add")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue("email", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"domain.management.edit.email.not.empty.message"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>{t("email.list")}</SelectLabel>
|
|
||||||
{(emails.content as EmailsSetting).emails.map(
|
|
||||||
(item) => (
|
|
||||||
<SelectItem key={item} value={item}>
|
|
||||||
<div>{item}</div>
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="access"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem hidden={tab != "base"}>
|
|
||||||
<FormLabel className="flex w-full justify-between">
|
|
||||||
<div>
|
|
||||||
{t("domain.management.edit.dns.access.label")}
|
|
||||||
</div>
|
|
||||||
<AccessEdit
|
|
||||||
trigger={
|
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
|
||||||
<Plus size={14} />
|
|
||||||
{t("add")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
op="add"
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue("access", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"domain.management.edit.access.not.empty.message"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>
|
|
||||||
{t("domain.management.edit.access.label")}
|
|
||||||
</SelectLabel>
|
|
||||||
{accesses
|
|
||||||
.filter((item) => item.usage != "deploy")
|
|
||||||
.map((item) => (
|
|
||||||
<SelectItem key={item.id} value={item.id}>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<img
|
|
||||||
className="w-6"
|
|
||||||
src={
|
|
||||||
accessTypeMap.get(
|
|
||||||
item.configType
|
|
||||||
)?.[1]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div>{item.name}</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="targetType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem hidden={tab != "base"}>
|
|
||||||
<FormLabel>
|
|
||||||
{t("domain.management.edit.target.type")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setTargetType(value);
|
|
||||||
form.setValue("targetType", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"domain.management.edit.target.type.not.empty.message"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>
|
|
||||||
{t("domain.management.edit.target.type")}
|
|
||||||
</SelectLabel>
|
|
||||||
{targetTypeKeys.map((key) => (
|
|
||||||
<SelectItem key={key} value={key}>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<img
|
|
||||||
className="w-6"
|
|
||||||
src={targetTypeMap.get(key)?.[1]}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{t(targetTypeMap.get(key)?.[0] || "")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="targetAccess"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem hidden={tab != "base"}>
|
|
||||||
<FormLabel className="w-full flex justify-between">
|
|
||||||
<div>{t("domain.management.edit.target.access")}</div>
|
|
||||||
<AccessEdit
|
|
||||||
trigger={
|
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
|
||||||
<Plus size={14} />
|
|
||||||
{t("add")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
op="add"
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue("targetAccess", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"domain.management.edit.target.access.not.empty.message"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>
|
|
||||||
{t(
|
|
||||||
"domain.management.edit.target.access.content.label"
|
|
||||||
)}{" "}
|
|
||||||
{form.getValues().targetAccess}
|
|
||||||
</SelectLabel>
|
|
||||||
<SelectItem value="emptyId">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
--
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
{targetAccesses.map((item) => (
|
|
||||||
<SelectItem key={item.id} value={item.id}>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<img
|
|
||||||
className="w-6"
|
|
||||||
src={
|
|
||||||
accessTypeMap.get(item.configType)?.[1]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div>{item.name}</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="group"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
|
|
||||||
<FormLabel className="w-full flex justify-between">
|
|
||||||
<div>{t("domain.management.edit.group.label")}</div>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
defaultValue="emptyId"
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue("group", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"domain.management.edit.group.not.empty.message"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="emptyId">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center space-x-2 rounded cursor-pointer"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
--
|
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
}
|
||||||
{accessGroups
|
/>
|
||||||
.filter((item) => {
|
</FormLabel>
|
||||||
return (
|
<FormControl>
|
||||||
item.expand && item.expand?.access.length > 0
|
<Select
|
||||||
);
|
{...field}
|
||||||
})
|
value={field.value}
|
||||||
.map((item) => (
|
onValueChange={(value) => {
|
||||||
<SelectItem
|
form.setValue("email", value);
|
||||||
value={item.id ? item.id : ""}
|
}}
|
||||||
key={item.id}
|
>
|
||||||
>
|
<SelectTrigger>
|
||||||
<div
|
<SelectValue
|
||||||
className={cn(
|
placeholder={t(
|
||||||
"flex items-center space-x-2 rounded cursor-pointer"
|
"domain.management.edit.email.not.empty.message"
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
{item.name}
|
</SelectTrigger>
|
||||||
</div>
|
<SelectContent>
|
||||||
</SelectItem>
|
<SelectGroup>
|
||||||
))}
|
<SelectLabel>{t("email.list")}</SelectLabel>
|
||||||
</SelectContent>
|
{(emails.content as EmailsSetting).emails.map(
|
||||||
</Select>
|
(item) => (
|
||||||
</FormControl>
|
<SelectItem key={item} value={item}>
|
||||||
|
<div>{item}</div>
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
{/* 授权 */}
|
||||||
control={form.control}
|
<FormField
|
||||||
name="variables"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="access"
|
||||||
<FormItem hidden={tab != "advance"}>
|
render={({ field }) => (
|
||||||
<FormLabel>{t("variables")}</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel className="flex w-full justify-between">
|
||||||
<Textarea
|
<div>
|
||||||
placeholder={t(
|
{t("domain.management.edit.dns.access.label")}
|
||||||
"domain.management.edit.variables.placeholder"
|
</div>
|
||||||
)}
|
<AccessEdit
|
||||||
{...field}
|
trigger={
|
||||||
className="placeholder:whitespace-pre-wrap"
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
/>
|
<Plus size={14} />
|
||||||
</FormControl>
|
{t("add")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
op="add"
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue("access", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.access.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>
|
||||||
|
{t("domain.management.edit.access.label")}
|
||||||
|
</SelectLabel>
|
||||||
|
{accesses
|
||||||
|
.filter((item) => item.usage != "deploy")
|
||||||
|
.map((item) => (
|
||||||
|
<SelectItem key={item.id} value={item.id}>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
className="w-6"
|
||||||
|
src={
|
||||||
|
accessTypeMap.get(
|
||||||
|
item.configType
|
||||||
|
)?.[1]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>{item.name}</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{/* 超时时间 */}
|
||||||
control={form.control}
|
<FormField
|
||||||
name="nameservers"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="timeout"
|
||||||
<FormItem hidden={tab != "advance"}>
|
render={({ field }) => (
|
||||||
<FormLabel>{t("dns")}</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>{t("timeout")}</FormLabel>
|
||||||
<Textarea
|
<FormControl>
|
||||||
placeholder={t(
|
<Input
|
||||||
"domain.management.edit.dns.placeholder"
|
type="number"
|
||||||
)}
|
placeholder={t(
|
||||||
{...field}
|
"domain.management.edit.timeout.placeholder"
|
||||||
className="placeholder:whitespace-pre-wrap"
|
)}
|
||||||
/>
|
{...field}
|
||||||
</FormControl>
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.setValue(
|
||||||
|
"timeout",
|
||||||
|
parseInt(e.target.value)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
{/* nameservers */}
|
||||||
<Button type="submit">{t("save")}</Button>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
</form>
|
name="nameservers"
|
||||||
</Form>
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<StringList
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onValueChange={(val: string) => {
|
||||||
|
form.setValue("nameservers", val);
|
||||||
|
}}
|
||||||
|
valueType="dns"
|
||||||
|
></StringList>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit">
|
||||||
|
{domain?.id ? t("save") : t("next")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-5 w-full md:w-[35em]",
|
||||||
|
tab == "apply" && "hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DeployList
|
||||||
|
deploys={domain?.deployConfig ?? []}
|
||||||
|
onChange={(list: DeployConfig[]) => {
|
||||||
|
handelOnDeployListChange(list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -113,8 +113,8 @@ const Home = () => {
|
||||||
|
|
||||||
const handleRightNowClick = async (domain: Domain) => {
|
const handleRightNowClick = async (domain: Domain) => {
|
||||||
try {
|
try {
|
||||||
unsubscribeId(domain.id);
|
unsubscribeId(domain.id ?? "");
|
||||||
subscribeId(domain.id, (resp) => {
|
subscribeId(domain.id ?? "", (resp) => {
|
||||||
console.log(resp);
|
console.log(resp);
|
||||||
const updatedDomains = domains.map((domain) => {
|
const updatedDomains = domains.map((domain) => {
|
||||||
if (domain.id === resp.id) {
|
if (domain.id === resp.id) {
|
||||||
|
@ -282,7 +282,7 @@ const Home = () => {
|
||||||
<Switch
|
<Switch
|
||||||
checked={domain.enabled}
|
checked={domain.enabled}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
handelCheckedChange(domain.id);
|
handelCheckedChange(domain.id ?? "");
|
||||||
}}
|
}}
|
||||||
></Switch>
|
></Switch>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
@ -298,7 +298,7 @@ const Home = () => {
|
||||||
<Button
|
<Button
|
||||||
variant={"link"}
|
variant={"link"}
|
||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleHistoryClick(domain.id)}
|
onClick={() => handleHistoryClick(domain.id ?? "")}
|
||||||
>
|
>
|
||||||
{t("deployment.log.name")}
|
{t("deployment.log.name")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -363,7 +363,7 @@ const Home = () => {
|
||||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteClick(domain.id);
|
handleDeleteClick(domain.id ?? "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("confirm")}
|
{t("confirm")}
|
||||||
|
@ -376,7 +376,7 @@ const Home = () => {
|
||||||
<Button
|
<Button
|
||||||
variant={"link"}
|
variant={"link"}
|
||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleEditClick(domain.id)}
|
onClick={() => handleEditClick(domain.id ?? "")}
|
||||||
>
|
>
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
Loading…
Reference in New Issue